diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ff00614..77c92c4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,10 +3,17 @@ on: push: branches: - "main" + pull_request: + branches: + - main jobs: Lint: runs-on: ubuntu-latest + env: + NEXT_PUBLIC_IS_DEPLOYMENT: true + NEXT_PUBLIC_NETWORK_TYPE: "testnet" + steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/README.md b/README.md index 6ad8368..08331fd 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ with Node.js v20. Once you have it installed, you can run: npm install npm run dev + ``` It integrates the `@burnt-labs/abstraxion` library from diff --git a/package-lock.json b/package-lock.json index 2be0f02..b39454b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10512,6 +10512,126 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", + "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", + "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", + "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", + "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", + "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", + "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", + "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", + "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..198784c --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 912d640..2ae1d01 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,7 +7,8 @@ import { Analytics } from "@vercel/analytics/react"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import { dashboardUrl, faucetContractAddress, rpcEndpoint } from "@/constants"; +import { FAUCET_CONTRACT_ADDRESS } from "@/config"; +import { REST_URL, RPC_URL } from "@/constants"; import BaseWrapper from "@/features/core/components/base-wrapper"; import { CoreProvider } from "@/features/core/context/provider"; import { StakingProvider } from "@/features/staking/context/provider"; @@ -15,9 +16,9 @@ import { StakingProvider } from "@/features/staking/context/provider"; import "./globals.css"; const abstraxionConfig = { - contracts: [faucetContractAddress], - dashboardUrl, - rpcUrl: rpcEndpoint, + contracts: [FAUCET_CONTRACT_ADDRESS], + restUrl: REST_URL, + rpcUrl: RPC_URL, stake: true, }; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..0aa0c68 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,35 @@ +function getEnvStringOrThrow(key: string, value?: string): string { + if (!value) { + throw new Error(`Environment variable ${key} must be defined`); + } + + return value; +} + +// Write helper function to get boolean environment variable +function getEnvBooleanOrThrow(key: string, value?: string): boolean { + if (!value) { + throw new Error(`Environment variable ${key} must be defined`); + } + + return value === "true"; +} + +// The base path for the deployment +export const BASE_PATH = getEnvBooleanOrThrow( + "NEXT_PUBLIC_IS_DEPLOYMENT", + process.env.NEXT_PUBLIC_IS_DEPLOYMENT, +) + ? "/xion-staking" + : ""; + +// The contract address for the faucet +export const FAUCET_CONTRACT_ADDRESS = + "xion1mczdpmlc2lcng2ktly3fapdc24zqhxsyn5eek8uu3egmrd97c73qqtss3u"; + +const NETWORK_TYPE = getEnvStringOrThrow( + "NEXT_PUBLIC_NETWORK_TYPE", + process.env.NEXT_PUBLIC_NETWORK_TYPE, +); + +export const IS_TESTNET = NETWORK_TYPE === "testnet"; diff --git a/src/constants.ts b/src/constants.ts index 9dd78eb..1a5f20c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,30 +1,21 @@ -export const isTestnet = true; +import { IS_TESTNET } from "./config"; -// // For local testing -// export const dashboardUrl = -// process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000"; +const MAINNET_RPC_URL = "https://rpc.xion-mainnet-1.burnt.com:443"; +const MAINNET_REST_URL = "https://api.xion-mainnet-1.burnt.com:443"; +const TESTNET_RPC_URL = "https://testnet-rpc.xion-api.com:443"; +const TESTNET_REST_URL = "https://testnet-api.xion-api.com:443"; -export const dashboardUrl = undefined; +export const RPC_URL = IS_TESTNET ? TESTNET_RPC_URL : MAINNET_RPC_URL; +export const REST_URL = IS_TESTNET ? TESTNET_REST_URL : MAINNET_REST_URL; -export const rpcEndpoint = "https://rpc.xion-testnet-1.burnt.com:443"; -// export const rpcEndpoint = "https://rpc.xion-testnet.forbole.com"; - -// This only exists on testnet. -export const faucetContractAddress = - "xion132dxh4k3dpyalq6tfq7006h8kpk3m30f4mwc5dgqefy6akudm50s96mn6q"; - -export const basePath = - process.env.NEXT_PUBLIC_IS_DEPLOYMENT === "true" ? "/xion-staking" : ""; - -export const xionToUSD = 10; +export const XION_TO_USD = 10; // // Even if this can be retrieved from the params, hardcode it to avoid an // // extra request. It can be retrieved with this: // const params = await queryClient.staking.params(); -export const unbondingDays = isTestnet ? 3 : 21; +export const UNBONDING_DAYS = IS_TESTNET ? 3 : 21; // Arbitrary value to avoid using a bigger fee than the actual reward -export const minClaimableXion = 0.00001; - -export const minDisplayedXionDecs = 6; -export const minDisplayedXion = 10 ** -minDisplayedXionDecs; +export const MIN_CLAIMABLE_XION = 0.00001; +export const MIN_DISPLAYED_XION_DECIMALS = 6; +export const MIN_DISPLAYED_XION = 10 ** -MIN_DISPLAYED_XION_DECIMALS; diff --git a/src/features/core/components/base-wrapper.tsx b/src/features/core/components/base-wrapper.tsx index 85aee11..39582a3 100644 --- a/src/features/core/components/base-wrapper.tsx +++ b/src/features/core/components/base-wrapper.tsx @@ -3,7 +3,7 @@ import { Abstraxion, useModal } from "@burnt-labs/abstraxion"; import Link from "next/link"; -import { basePath, isTestnet } from "@/constants"; +import { BASE_PATH, IS_TESTNET } from "@/config"; import NavAccount from "./nav-account"; @@ -23,17 +23,17 @@ export default function RootLayout({
- Xion Logo + Xion Logo - {isTestnet ? "Testnet" : "Mainnet"} + {IS_TESTNET ? "Testnet" : "Mainnet"}
diff --git a/src/features/core/components/base.tsx b/src/features/core/components/base.tsx index 8ab5360..17a9a7a 100644 --- a/src/features/core/components/base.tsx +++ b/src/features/core/components/base.tsx @@ -296,7 +296,6 @@ export const FloatingDropdown = ({ offset={offset} open={isOpen} placement={placement} - withTransition > {children} diff --git a/src/features/core/components/nav-account.tsx b/src/features/core/components/nav-account.tsx index 33678e5..9141fe6 100644 --- a/src/features/core/components/nav-account.tsx +++ b/src/features/core/components/nav-account.tsx @@ -28,6 +28,7 @@ const NavAccount = () => {
diff --git a/src/features/staking/components/faucet.tsx b/src/features/staking/components/faucet.tsx index 56b4e57..791b593 100644 --- a/src/features/staking/components/faucet.tsx +++ b/src/features/staking/components/faucet.tsx @@ -1,7 +1,7 @@ import { useAbstraxionAccount } from "@burnt-labs/abstraxion"; import { memo, useCallback, useEffect, useState } from "react"; -import { isTestnet } from "@/constants"; +import { IS_TESTNET } from "@/config"; import { Button } from "@/features/core/components/base"; import { fetchUserDataAction } from "@/features/staking/context/actions"; import { normaliseCoin } from "@/features/staking/lib/core/coins"; @@ -30,7 +30,7 @@ const Faucet = () => { const result = await getAddressLastFaucetTimestamp(address, client); // We need to hide this when not on testnet. - if (staking.state.tokens?.denom !== result.denom || !isTestnet) { + if (staking.state.tokens?.denom !== result.denom || !IS_TESTNET) { return; } diff --git a/src/features/staking/components/main-page.tsx b/src/features/staking/components/main-page.tsx index f52fbf6..aa2e51d 100644 --- a/src/features/staking/components/main-page.tsx +++ b/src/features/staking/components/main-page.tsx @@ -2,6 +2,7 @@ import { memo, useState } from "react"; +import { IS_TESTNET } from "@/config"; import { Title } from "@/features/core/components/base"; import { useStaking } from "../context/hooks"; @@ -35,7 +36,7 @@ function StakingPage() { /> )}
- + {IS_TESTNET && } {isShowingDetails && canShowDetail && } diff --git a/src/features/staking/components/modals/redelegate.tsx b/src/features/staking/components/modals/redelegate.tsx index 5550798..4a50f37 100644 --- a/src/features/staking/components/modals/redelegate.tsx +++ b/src/features/staking/components/modals/redelegate.tsx @@ -6,7 +6,7 @@ import type { FormEventHandler } from "react"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { unbondingDays, xionToUSD } from "@/constants"; +import { UNBONDING_DAYS, XION_TO_USD } from "@/constants"; import { Button, FormError, @@ -150,7 +150,7 @@ const RedelegateModal = () => { const amountUSD = (() => { if (amountXIONParsed.isNaN()) return ""; - return amountXIONParsed.times(xionToUSD); + return amountXIONParsed.times(XION_TO_USD); })(); const delegatedTokens = getTotalDelegation( @@ -251,7 +251,7 @@ const RedelegateModal = () => { You are about to redelegate your token from{" "} {validator.description.moniker} to {dstValidator?.description.moniker}. Remember, you will not - able to redelegate these token within {unbondingDays} days. + able to redelegate these token within {UNBONDING_DAYS} days.
{getUnstakingSummary()} diff --git a/src/features/staking/components/modals/rewards.tsx b/src/features/staking/components/modals/rewards.tsx index 7f7537f..b9e7213 100644 --- a/src/features/staking/components/modals/rewards.tsx +++ b/src/features/staking/components/modals/rewards.tsx @@ -2,7 +2,7 @@ import BigNumber from "bignumber.js"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-toastify"; -import { minClaimableXion } from "@/constants"; +import { MIN_CLAIMABLE_XION } from "@/constants"; import { Button, HeroText } from "@/features/core/components/base"; import CommonModal, { ModalDescription, @@ -39,7 +39,7 @@ const claimRewardsLoop = async ( const normalised = normaliseCoin(delegation.rewards); - if (new BigNumber(normalised.amount).lt(minClaimableXion)) { + if (new BigNumber(normalised.amount).lt(MIN_CLAIMABLE_XION)) { return; } diff --git a/src/features/staking/components/modals/staking.tsx b/src/features/staking/components/modals/staking.tsx index c170b36..8a49b51 100644 --- a/src/features/staking/components/modals/staking.tsx +++ b/src/features/staking/components/modals/staking.tsx @@ -3,7 +3,7 @@ import type { FormEventHandler } from "react"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { xionToUSD } from "@/constants"; +import { XION_TO_USD } from "@/constants"; import { Button, FormError, @@ -71,7 +71,7 @@ const StakingModal = () => { const amountUSD = (() => { if (amountXIONParsed.isNaN()) return ""; - return amountXIONParsed.times(xionToUSD); + return amountXIONParsed.times(XION_TO_USD); })(); const hasErrors = Object.values(formError).some((v) => !!v); diff --git a/src/features/staking/components/modals/unstaking.tsx b/src/features/staking/components/modals/unstaking.tsx index cf5a549..6e25438 100644 --- a/src/features/staking/components/modals/unstaking.tsx +++ b/src/features/staking/components/modals/unstaking.tsx @@ -3,7 +3,7 @@ import type { FormEventHandler } from "react"; import { useEffect, useState } from "react"; import { toast } from "react-toastify"; -import { unbondingDays, xionToUSD } from "@/constants"; +import { UNBONDING_DAYS, XION_TO_USD } from "@/constants"; import { Button, FormError, @@ -71,7 +71,7 @@ const UnstakingModal = () => { const amountUSD = (() => { if (amountXIONParsed.isNaN()) return ""; - return amountXIONParsed.times(xionToUSD); + return amountXIONParsed.times(XION_TO_USD); })(); const delegatedTokens = getTotalDelegation( @@ -143,7 +143,7 @@ const UnstakingModal = () => { You have successfully unstaked from{" "} - {validator.description.moniker}. It takes {unbondingDays}{" "} + {validator.description.moniker}. It takes {UNBONDING_DAYS}{" "} days to complete the unstaking process @@ -169,7 +169,7 @@ const UnstakingModal = () => { Unstaking your XION Token means you'll stop earning rewards. - Remember, it takes {unbondingDays} days to complete the + Remember, it takes {UNBONDING_DAYS} days to complete the unstaking process. diff --git a/src/features/staking/components/staking-overview.tsx b/src/features/staking/components/staking-overview.tsx index 2247a57..3641179 100644 --- a/src/features/staking/components/staking-overview.tsx +++ b/src/features/staking/components/staking-overview.tsx @@ -2,7 +2,7 @@ import { useAbstraxionAccount, useModal } from "@burnt-labs/abstraxion"; import BigNumber from "bignumber.js"; import { memo } from "react"; -import { basePath } from "@/constants"; +import { BASE_PATH } from "@/config"; import { BodyMedium, Button, @@ -53,7 +53,7 @@ const StakingOverview = () => {
@@ -88,7 +88,7 @@ const StakingOverview = () => {
{formatToSmallDisplay( new BigNumber(totalRewards.amount), - minDisplayedXion, + MIN_DISPLAYED_XION, )} )} diff --git a/src/features/staking/components/validator-page.tsx b/src/features/staking/components/validator-page.tsx index f43113d..1254577 100644 --- a/src/features/staking/components/validator-page.tsx +++ b/src/features/staking/components/validator-page.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -import { basePath } from "@/constants"; +import { BASE_PATH } from "@/config"; import { BodyMedium, Button, @@ -108,7 +108,7 @@ export default function ValidatorPage() {
["client"] @@ -37,7 +37,7 @@ let stakingQueryClientPromise: export const getStakingQueryClient = () => { if (!stakingQueryClientPromise) { stakingQueryClientPromise = (async () => { - const cometClient = await Tendermint34Client.connect(rpcEndpoint); + const cometClient = await Tendermint34Client.connect(RPC_URL); return QueryClient.withExtensions( cometClient, @@ -57,7 +57,7 @@ let stargateClientPromise: Promise | undefined = undefined; export const getStargateClient = () => { if (!stargateClientPromise) { stargateClientPromise = (async () => { - const client = await StargateClient.connect(rpcEndpoint); + const client = await StargateClient.connect(RPC_URL); return client; })(); @@ -82,7 +82,7 @@ export const createLocalSigningClient = async (registry?: Registry) => { .then((accounts) => accounts[0]?.address); const client = await SigningStargateClient.connectWithSigner( - rpcEndpoint, + RPC_URL, wallet, { registry, diff --git a/src/features/staking/lib/core/tx.ts b/src/features/staking/lib/core/tx.ts index 99dffd0..50de7bf 100644 --- a/src/features/staking/lib/core/tx.ts +++ b/src/features/staking/lib/core/tx.ts @@ -15,7 +15,8 @@ import { MsgUndelegate, } from "cosmjs-types/cosmos/staking/v1beta1/tx"; -import { faucetContractAddress, minClaimableXion } from "@/constants"; +import { FAUCET_CONTRACT_ADDRESS } from "@/config"; +import { MIN_CLAIMABLE_XION } from "@/constants"; import type { Unbonding } from "../../context/state"; import { type AbstraxionSigningClient } from "./client"; @@ -204,7 +205,7 @@ export const getCanClaimRewards = (rewards?: Coin) => { const normalised = normaliseCoin(rewards); - return new BigNumber(normalised.amount).gte(minClaimableXion); + return new BigNumber(normalised.amount).gte(MIN_CLAIMABLE_XION); }; export const cancelUnbonding = async ( @@ -269,7 +270,7 @@ export const getAddressLastFaucetTimestamp = async ( }; return await client - .queryContractSmart(faucetContractAddress, msg) + .queryContractSmart(FAUCET_CONTRACT_ADDRESS, msg) .then((res: GetAccountLastClaimTimestampResponse) => { // Get the current timestamp in seconds const currentTimestampInSeconds = Math.floor(Date.now() / 1000); @@ -306,6 +307,6 @@ export const faucetFunds = async ( }; return await client - .execute(address, faucetContractAddress, msg, "auto") + .execute(address, FAUCET_CONTRACT_ADDRESS, msg, "auto") .catch(handleTxError); }; diff --git a/src/features/staking/lib/formatters.ts b/src/features/staking/lib/formatters.ts index 4222dfc..9b34e79 100644 --- a/src/features/staking/lib/formatters.ts +++ b/src/features/staking/lib/formatters.ts @@ -1,7 +1,11 @@ import type { Coin } from "@cosmjs/stargate"; import BigNumber from "bignumber.js"; -import { minDisplayedXion, minDisplayedXionDecs, xionToUSD } from "@/constants"; +import { + MIN_DISPLAYED_XION, + MIN_DISPLAYED_XION_DECIMALS, + XION_TO_USD, +} from "@/constants"; import { getEmptyXionCoin, normaliseCoin } from "./core/coins"; @@ -18,8 +22,8 @@ export const formatCoin = ( return `${amount.toFormat()}${denomSuffix}`; } - if (amount.lt(minDisplayedXion)) { - return `<${minDisplayedXion}${denomSuffix}`; + if (amount.lt(MIN_DISPLAYED_XION)) { + return `<${MIN_DISPLAYED_XION}${denomSuffix}`; } if (compact) { @@ -30,7 +34,7 @@ export const formatCoin = ( return `${formatter.format(amount.toNumber())}${denomSuffix}`; } - return `${amount.toFormat(Math.min(minDisplayedXionDecs, amount.decimalPlaces() || Infinity))}${denomSuffix}`; + return `${amount.toFormat(Math.min(MIN_DISPLAYED_XION_DECIMALS, amount.decimalPlaces() || Infinity))}${denomSuffix}`; }; export const formatVotingPowerPerc = (perc: null | number) => { @@ -72,7 +76,7 @@ export const formatCommission = (commissionRate: string, decimals: number) => { export const formatXionToUSD = (coin: Coin | null, compact?: boolean) => { const normalised = normaliseCoin(coin || getEmptyXionCoin()); const value = coin ? new BigNumber(normalised.amount) : new BigNumber(0); - const usd = value.times(xionToUSD); + const usd = value.times(XION_TO_USD); if (usd.eq(0)) { return "$0";