From 2ef22663bb5d0e643e3b3e45b112b4eeef1df736 Mon Sep 17 00:00:00 2001 From: Justin <328965+justinbarry@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:35:00 -0700 Subject: [PATCH] Add testnet faucet widget (#1) --- src/app/layout.tsx | 6 +- src/constants.ts | 4 + src/features/staking/components/faucet.tsx | 93 +++++++++++++++++++ src/features/staking/components/main-page.tsx | 2 + src/features/staking/lib/core/tx.ts | 70 +++++++++++++- 5 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/features/staking/components/faucet.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f306226..439cad9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,7 +6,7 @@ import "@burnt-labs/ui/dist/index.css"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; -import { dashboardUrl, rpcEndpoint } from "@/constants"; +import { dashboardUrl, faucetContractAddress, rpcEndpoint } from "@/constants"; import BaseWrapper from "@/features/core/components/base-wrapper"; import { CoreProvider } from "@/features/core/context/provider"; import { StakingProvider } from "@/features/staking/context/provider"; @@ -14,7 +14,9 @@ import { StakingProvider } from "@/features/staking/context/provider"; import "./globals.css"; const abstraxionConfig = { - contracts: [], + contracts: [ + faucetContractAddress + ], dashboardUrl, rpcUrl: rpcEndpoint, stake: true, diff --git a/src/constants.ts b/src/constants.ts index c2a0cb3..05e9be9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,10 @@ export const dashboardUrl = undefined; // 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 = + "xion1z8gr3jn7rd5leyvz7z3zmelxmrz6q8lv7flc3a4u8kltwj6leags8u64qx"; + export const basePath = process.env.NEXT_PUBLIC_IS_DEPLOYMENT === "true" ? "/xion-staking" : ""; diff --git a/src/features/staking/components/faucet.tsx b/src/features/staking/components/faucet.tsx new file mode 100644 index 0000000..bdda954 --- /dev/null +++ b/src/features/staking/components/faucet.tsx @@ -0,0 +1,93 @@ +import { useAbstraxionAccount } from "@burnt-labs/abstraxion"; +import { memo, useCallback, useEffect, useState } from "react"; + +import { isTestnet } from "@/constants"; +import { Button } from "@/features/core/components/base"; +import { fetchUserDataAction } from "@/features/staking/context/actions"; +import { normaliseCoin } from "@/features/staking/lib/core/coins"; + +import { useStaking } from "../context/hooks"; +import type { AddressLastFaucetStatus } from "../lib/core/tx"; +import { faucetFunds, getAddressLastFaucetTimestamp } from "../lib/core/tx"; + +const Faucet = () => { + const { isConnected } = useAbstraxionAccount(); + const { address, client, staking } = useStaking(); + + const [lastFaucetStatus, setLastFaucetStatus] = + useState({ + canFaucet: false, + denom: "", + lastFaucetTimestamp: 0, + maxBalance: 0, + nextFaucetTimestamp: 0, + }); + + const [isFauceting, setIsFauceting] = useState(false); + + const updateFaucetStatus = useCallback(async () => { + if (isConnected && address && client) { + const result = await getAddressLastFaucetTimestamp(address, client); + + // We need to hide this when not on testnet. + if (staking.state.tokens?.denom !== result.denom || !isTestnet) { + return; + } + + if (parseInt(staking.state.tokens?.amount) > result.maxBalance) { + setLastFaucetStatus({ + ...result, + + canFaucet: false, + }); + } else { + setLastFaucetStatus(result); + } + } + }, [isConnected, address, client, staking.state.tokens]); + + useEffect(() => { + updateFaucetStatus(); + }, [isConnected, address, client, updateFaucetStatus]); + + if (!isConnected || !lastFaucetStatus.canFaucet) { + return null; + } + + const normalizedFaucetInfo = normaliseCoin({ + amount: lastFaucetStatus.maxBalance.toString(), + denom: lastFaucetStatus.denom, + }); + + return ( +
+ {lastFaucetStatus.canFaucet && ( + + )} +
+ ); +}; + +export default memo(Faucet); diff --git a/src/features/staking/components/main-page.tsx b/src/features/staking/components/main-page.tsx index bca1ccc..f20d742 100644 --- a/src/features/staking/components/main-page.tsx +++ b/src/features/staking/components/main-page.tsx @@ -12,6 +12,7 @@ import DelegationDetails, { import StakingModals from "./staking-modals"; import StakingOverview from "./staking-overview"; import ValidatorsTable from "./validators-table"; +import Faucet from "./faucet"; function StakingPage() { const { staking } = useStaking(); @@ -31,6 +32,7 @@ function StakingPage() { /> )} + {isShowingDetails && canShowDetail && } diff --git a/src/features/staking/lib/core/tx.ts b/src/features/staking/lib/core/tx.ts index 935dc66..b9668a2 100644 --- a/src/features/staking/lib/core/tx.ts +++ b/src/features/staking/lib/core/tx.ts @@ -13,7 +13,7 @@ import { MsgUndelegate, } from "cosmjs-types/cosmos/staking/v1beta1/tx"; -import { minClaimableXion } from "@/constants"; +import { faucetContractAddress, minClaimableXion } from "@/constants"; import type { Unbonding } from "../../context/state"; import { type AbstraxionSigningClient } from "./client"; @@ -196,3 +196,71 @@ export const cancelUnbonding = async ( .then(getTxVerifier("cancel_unbonding_delegation")) .catch(handleTxError); }; + +export interface AddressLastFaucetStatus { + canFaucet: boolean; + + denom: string; + lastFaucetTimestamp: number; + maxBalance: number; + nextFaucetTimestamp: number; +} + +interface GetAccountLastClaimTimestampResponse { + amount_to_faucet: number; + cooldown_period: number; + denom: string; + timestamp: string; +} + +export const getAddressLastFaucetTimestamp = async ( + address: string, + client: NonNullable, +): Promise => { + const msg = { + get_address_last_faucet_timestamp: { + address, + }, + }; + + return await client + .queryContractSmart(faucetContractAddress, msg) + .then((res: GetAccountLastClaimTimestampResponse) => { + // Get the current timestamp in seconds + const currentTimestampInSeconds = Math.floor(Date.now() / 1000); + const timestamp = parseInt(res.timestamp); + + // If the timestamp is 0, the user has never claimed. + if (timestamp === 0) { + return { + canFaucet: true, + denom: res.denom, + lastFaucetTimestamp: 0, + maxBalance: res.amount_to_faucet, + nextFaucetTimestamp: currentTimestampInSeconds, + }; + } + + return { + canFaucet: timestamp + res.cooldown_period < currentTimestampInSeconds, + denom: res.denom, + lastFaucetTimestamp: timestamp, + maxBalance: res.amount_to_faucet, + nextFaucetTimestamp: timestamp + res.cooldown_period, + }; + }) + .catch(handleTxError); +}; + +export const faucetFunds = async ( + address: string, + client: NonNullable, +) => { + const msg = { + faucet: {}, + }; + + return await client + .execute(address, faucetContractAddress, msg, "auto") + .catch(handleTxError); +};