Skip to content

Commit

Permalink
Add testnet faucet widget (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinbarry authored Mar 18, 2024
1 parent 45d8a4f commit 2ef2266
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 3 deletions.
6 changes: 4 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ 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";

import "./globals.css";

const abstraxionConfig = {
contracts: [],
contracts: [
faucetContractAddress
],
dashboardUrl,
rpcUrl: rpcEndpoint,
stake: true,
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" : "";

Expand Down
93 changes: 93 additions & 0 deletions src/features/staking/components/faucet.tsx
Original file line number Diff line number Diff line change
@@ -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<AddressLastFaucetStatus>({
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 (
<div
className="grid min-h-[144px] flex-col items-center justify-center gap-[8px] overflow-auto"
style={{
gridTemplateColumns: "1fr",
}}
>
{lastFaucetStatus.canFaucet && (
<Button
isLoading={isFauceting}
onClick={async () => {
if (!client) return;

try {
setIsFauceting(true);
await faucetFunds(address, client);
} catch (e) {
console.error(e);
} finally {
await fetchUserDataAction(address, staking);
setIsFauceting(false);
}
}}
>
Faucet {normalizedFaucetInfo.amount} {normalizedFaucetInfo.denom}
</Button>
)}
</div>
);
};

export default memo(Faucet);
2 changes: 2 additions & 0 deletions src/features/staking/components/main-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -31,6 +32,7 @@ function StakingPage() {
/>
)}
</div>
<Faucet />
<StakingOverview />
{isShowingDetails && canShowDetail && <DelegationDetails />}
<ValidatorsTable />
Expand Down
70 changes: 69 additions & 1 deletion src/features/staking/lib/core/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<AbstraxionSigningClient>,
): Promise<AddressLastFaucetStatus> => {
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<AbstraxionSigningClient>,
) => {
const msg = {
faucet: {},
};

return await client
.execute(address, faucetContractAddress, msg, "auto")
.catch(handleTxError);
};

0 comments on commit 2ef2266

Please sign in to comment.