diff --git a/package-lock.json b/package-lock.json index a558b01..f33356b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "cosmjs-types": "^0.9.0", "next": "14.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-toastify": "^10.0.4" }, "devDependencies": { "@stylistic/eslint-plugin": "^1.6.2", @@ -4146,6 +4147,14 @@ "node": ">=8" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -8447,6 +8456,18 @@ } } }, + "node_modules/react-toastify": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.4.tgz", + "integrity": "sha512-etR3RgueY8pe88SA67wLm8rJmL1h+CLqUGHuAoNsseW35oTGJEri6eBTyaXnFKNQ80v/eO10hBYLgz036XRGgA==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 717c46a..298d088 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "cosmjs-types": "^0.9.0", "next": "14.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-toastify": "^10.0.4" }, "devDependencies": { "@stylistic/eslint-plugin": "^1.6.2", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 74e2026..2b16ebc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,8 @@ import { AbstraxionProvider } from "@burnt-labs/abstraxion"; import "@burnt-labs/abstraxion/dist/index.css"; import "@burnt-labs/ui/dist/index.css"; import { Inter } from "next/font/google"; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import { StakingProvider } from "@/features/staking/context/provider"; import { @@ -33,6 +35,7 @@ export default function RootLayout({ {children} + ); diff --git a/src/features/staking/components/logged-in.tsx b/src/features/staking/components/logged-in.tsx index 24c9934..e28e06f 100644 --- a/src/features/staking/components/logged-in.tsx +++ b/src/features/staking/components/logged-in.tsx @@ -5,6 +5,7 @@ import { Button } from "@burnt-labs/ui"; import type { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import Link from "next/link"; import { memo, useMemo, useState } from "react"; +import { toast } from "react-toastify"; import { claimRewardsAction, @@ -57,7 +58,10 @@ const ValidatorItem = ({ function StakingPage() { const { account, staking } = useStaking(); const [isLoading, setIsLoading] = useState(false); - const { delegations, tokens, unbondings, validators } = staking.state; + + const { delegations, isInfoLoading, tokens, unbondings, validators } = + staking.state; + const { client } = useAbstraxionSigningClient(); const [, setShowAbstraxion] = useModal(); @@ -96,6 +100,7 @@ function StakingPage() { )} + {isInfoLoading &&
Loading ...
} {!!delegations?.items.length && (
Delegations:
@@ -126,13 +131,20 @@ function StakingPage() { validator: validator.operatorAddress, }; - unstakeValidatorAction( - addresses, - client, - staking, - ).finally(() => { - setIsLoading(false); - }); + unstakeValidatorAction(addresses, client, staking) + .then(() => { + toast("Unstake successful", { + type: "success", + }); + }) + .catch(() => { + toast("Unstake error", { + type: "error", + }); + }) + .finally(() => { + setIsLoading(false); + }); }} > Undelegate @@ -150,11 +162,16 @@ function StakingPage() { validator: delegation.validatorAddress, }; - claimRewardsAction(addresses, client, staking).finally( - () => { + claimRewardsAction(addresses, client, staking) + .then(() => { + toast("Claim success", { type: "success" }); + }) + .catch(() => { + toast("Claim error", { type: "error" }); + }) + .finally(() => { setIsLoading(false); - }, - ); + }); }} > Claim rewards @@ -240,11 +257,20 @@ function StakingPage() { validator: validator.operatorAddress, }; - stakeValidatorAction(addresses, client, staking).finally( - () => { + stakeValidatorAction(addresses, client, staking) + .then(() => { + toast("Staking successful", { + type: "success", + }); + }) + .catch(() => { + toast("Staking error", { + type: "error", + }); + }) + .finally(() => { setIsLoading(false); - }, - ); + }); }} validator={validator} /> diff --git a/src/features/staking/context/actions.ts b/src/features/staking/context/actions.ts index fdad127..d75f4f1 100644 --- a/src/features/staking/context/actions.ts +++ b/src/features/staking/context/actions.ts @@ -15,6 +15,7 @@ import { sumAllCoins } from "../lib/core/coins"; import { addDelegations, addUnbondings, + setIsInfoLoading, setTokens, setValidators, } from "./reducer"; @@ -25,6 +26,8 @@ export const fetchStakingDataAction = async ( staking: StakingContextType, ) => { try { + staking.dispatch(setIsInfoLoading(true)); + const [balance, validators, delegations, unbondings] = await Promise.all([ getBalance(address), getValidatorsList(), @@ -102,6 +105,8 @@ export const fetchStakingDataAction = async ( true, ), ); + + staking.dispatch(setIsInfoLoading(false)); } catch (error) { console.error("error fetching staking data:", error); } @@ -125,14 +130,11 @@ export const unstakeValidatorAction = async ( client: AbstraxionSigningClient, staking: StakingContextType, ) => { - const result = await unstakeAmount(addresses, client, { + await unstakeAmount(addresses, client, { amount: "1000", denom: "uxion", }); - // eslint-disable-next-line no-console - console.log("debug: actions.ts: result", result); - await fetchStakingDataAction(addresses.delegator, staking); }; @@ -141,10 +143,7 @@ export const claimRewardsAction = async ( client: AbstraxionSigningClient, staking: StakingContextType, ) => { - const result = await claimRewards(addresses, client); - - // eslint-disable-next-line no-console - console.log("debug: actions.ts: result", result); + await claimRewards(addresses, client); await fetchStakingDataAction(addresses.delegator, staking); }; @@ -154,10 +153,7 @@ export const setRedelegateAction = async ( client: AbstraxionSigningClient, staking: StakingContextType, ) => { - const result = await setRedelegate(delegatorAddress, client); - - // eslint-disable-next-line no-console - console.log("debug: actions.ts: result", result); + await setRedelegate(delegatorAddress, client); await fetchStakingDataAction(delegatorAddress, staking); }; diff --git a/src/features/staking/context/hooks.ts b/src/features/staking/context/hooks.ts index 6e99337..b25e86d 100644 --- a/src/features/staking/context/hooks.ts +++ b/src/features/staking/context/hooks.ts @@ -10,7 +10,8 @@ export const useStaking = () => { const staking = useContext(StakingContext); // It is important to not override the `current` object reference so it - // doesn't trigger more hooks than it should + // doesn't trigger more hooks than it should if it is added as a hook + // dependency stakingRef.current.state = staking.state; stakingRef.current.dispatch = staking.dispatch; diff --git a/src/features/staking/context/reducer.ts b/src/features/staking/context/reducer.ts index 292dcc8..39da4a8 100644 --- a/src/features/staking/context/reducer.ts +++ b/src/features/staking/context/reducer.ts @@ -16,6 +16,10 @@ export type StakingAction = reset: boolean; type: "ADD_VALIDATORS"; } + | { + content: StakingState["isInfoLoading"]; + type: "SET_IS_INFO_LOADING"; + } | { content: StakingState["tokens"]; type: "SET_TOKENS"; @@ -31,6 +35,13 @@ export const setTokens = (tokens: Content<"SET_TOKENS">): StakingAction => ({ type: "SET_TOKENS", }); +export const setIsInfoLoading = ( + isInfoLoading: Content<"SET_IS_INFO_LOADING">, +): StakingAction => ({ + content: isInfoLoading, + type: "SET_IS_INFO_LOADING", +}); + export const setValidators = ( validators: Content<"ADD_VALIDATORS">, reset: boolean, @@ -174,6 +185,13 @@ export const reducer = (state: StakingState, action: StakingAction) => { }; } + case "SET_IS_INFO_LOADING": { + return { + ...state, + isInfoLoading: action.content, + }; + } + default: action satisfies never; diff --git a/src/features/staking/context/state.tsx b/src/features/staking/context/state.tsx index b3b8ce9..46b1583 100644 --- a/src/features/staking/context/state.tsx +++ b/src/features/staking/context/state.tsx @@ -27,6 +27,7 @@ type Delegation = { export type StakingState = { delegations: Paginated; + isInfoLoading: boolean; tokens: Coin | null; unbondings: Paginated; validators: Paginated; @@ -39,6 +40,7 @@ export type StakingContextType = { export const defaultState: StakingState = { delegations: null, + isInfoLoading: false, tokens: null, unbondings: null, validators: null, diff --git a/src/features/staking/lib/core/base.ts b/src/features/staking/lib/core/base.ts index d1228f8..b63b385 100644 --- a/src/features/staking/lib/core/base.ts +++ b/src/features/staking/lib/core/base.ts @@ -1,6 +1,7 @@ import { StargateClient } from "@cosmjs/stargate"; import type { Coin, + DeliverTxResponse, MsgBeginRedelegateEncodeObject, MsgDelegateEncodeObject, MsgUndelegateEncodeObject, @@ -32,6 +33,26 @@ export const getBalance = async (address: string) => { return await client.getBalance(address, "uxion"); }; +const getTxVerifier = (eventType: string) => (result: DeliverTxResponse) => { + // @TODO + // eslint-disable-next-line no-console + console.log("debug: base.ts: result", result); + + if (!result.events.find((e) => e.type === eventType)) { + console.error(result); + throw new Error("Out of gas"); + } + + return result; +}; + +const handleTxError = (err: unknown) => { + // eslint-disable-next-line no-console + console.error(err); + + throw err; +}; + export const getDelegations = async (address: string) => { const queryClient = await getStakingQueryClient(); @@ -103,11 +124,10 @@ export const stakeAmount = async ( msgs: [messageWrapper], }); - return await client.signAndBroadcast( - addresses.delegator, - [messageWrapper], - fee, - ); + return await client + .signAndBroadcast(addresses.delegator, [messageWrapper], fee) + .then(getTxVerifier("delegate")) + .catch(handleTxError); }; export const unstakeAmount = async ( @@ -131,11 +151,10 @@ export const unstakeAmount = async ( msgs: [messageWrapper], }); - return await client.signAndBroadcast( - addresses.delegator, - [messageWrapper], - fee, - ); + return await client + .signAndBroadcast(addresses.delegator, [messageWrapper], fee) + .then(getTxVerifier("unbond")) + .catch(handleTxError); }; export const getUnbonding = async ( @@ -156,21 +175,22 @@ export const claimRewards = async ( validatorAddress: addresses.validator, }); - const messageWrapper: MsgWithdrawDelegatorRewardEncodeObject = { - typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", - value: msg, - }; + const messageWrapper = [ + { + typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + value: msg, + } satisfies MsgWithdrawDelegatorRewardEncodeObject, + ]; const fee = await getCosmosFee({ address: addresses.delegator, - msgs: [messageWrapper], + msgs: messageWrapper, }); - return await client.signAndBroadcast( - addresses.delegator, - [messageWrapper], - fee, - ); + return await client + .signAndBroadcast(addresses.delegator, messageWrapper, fee) + .then(getTxVerifier("withdraw_rewards")) + .catch(handleTxError); }; // @TODO: Pass the target delegator diff --git a/src/features/staking/lib/core/fee.ts b/src/features/staking/lib/core/fee.ts index b0646d5..28eec66 100644 --- a/src/features/staking/lib/core/fee.ts +++ b/src/features/staking/lib/core/fee.ts @@ -24,9 +24,9 @@ const simulateMsgsWithExec = async (msgs: EncodeObject[], memo: string) => { .then((estimate) => { // This is a factor to increase the gas fee, since the estimate can be a // bit short in some cases (especially for the last events) - const gasFeeFactor = 1.2; + const gasFeeFactor = 2; - return (estimate * gasFeeFactor).toString(); + return Math.ceil(estimate * gasFeeFactor).toString(); }) .catch((err) => { // eslint-disable-next-line no-console