Skip to content

Commit

Permalink
feat: improve validator details
Browse files Browse the repository at this point in the history
  • Loading branch information
icfor committed Mar 1, 2024
1 parent 8648135 commit 693f842
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 358 deletions.
383 changes: 177 additions & 206 deletions src/features/staking/components/main-page.tsx

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions src/features/staking/components/validator-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { Button } from "@burnt-labs/ui";
import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
Expand All @@ -8,6 +9,7 @@ import { useEffect, useState } from "react";
import { getValidatorDetailsAction } from "../context/actions";
import { useStaking } from "../context/hooks";
import { getVotingPowerPerc } from "../context/selectors";
import { getValidatorExplorerLink } from "../lib/core/accounts";
import { formatCommission, formatVotingPowerPerc } from "../lib/formatters";
import { keybaseClient } from "../lib/utils/keybase-client";

Expand Down Expand Up @@ -63,10 +65,19 @@ export default function ValidatorPage() {
</div>
)}
<div>{validatorDetails.description.moniker}</div>
<div>
<Link
href={getValidatorExplorerLink(validatorDetails.operatorAddress)}
target="_blank"
>
<Button>View in Explorer</Button>
</Link>
</div>
<div>{validatorDetails.description.details}</div>
<div>{validatorDetails.description.identity}</div>
<div>Identity: {validatorDetails.description.identity}</div>
<div>{validatorDetails.description.securityContact}</div>
<div>{validatorDetails.description.website}</div>
<div>Pub key: {validatorDetails.consensusPubkey?.value}</div>
<div>
Commission:{" "}
{formatCommission(validatorDetails.commission.commissionRates.rate)}
Expand All @@ -89,7 +100,9 @@ export default function ValidatorPage() {
: validatorDetails.status}
</div>
{votingPowerPercStr && <div>Voting Power: {votingPowerPercStr}</div>}
<Link href="/">Back</Link>
<Link href="/">
<Button>Back</Button>
</Link>
</div>
);
}
77 changes: 77 additions & 0 deletions src/features/staking/components/validator-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client";

import { Button } from "@burnt-labs/ui";
import Link from "next/link";
import { memo, useEffect, useState } from "react";

import { getVotingPowerPerc } from "../context/selectors";
import type { StakingContextType, StakingState } from "../context/state";
import { formatVotingPowerPerc } from "../lib/formatters";
import { keybaseClient } from "../lib/utils/keybase-client";

type ValidatorItemProps = {
disabled?: boolean;
onStake: () => void;
staking: StakingContextType;
validator: NonNullable<StakingState["validators"]>["items"][number];
};

const ValidatorRow = ({
disabled,
onStake,
staking,
validator,
}: ValidatorItemProps) => {
const { website } = validator.description;
const [logo, setLogo] = useState<null | string>(null);

const { identity } = validator.description;

useEffect(() => {
(async () => {
if (identity) {
const logoResponse = await keybaseClient.getIdentityLogo(identity);

setLogo(logoResponse);
}
})();
}, [identity]);

const votingPowerPerc = getVotingPowerPerc(validator?.tokens, staking.state);
const votingPowerPercStr = formatVotingPowerPerc(votingPowerPerc);

return (
<div style={{ border: "solid 1px white", marginBottom: 10 }}>
{logo && (
<div>
<img
alt="Validator logo"
src={logo}
style={{ height: 50, width: 50 }}
/>
</div>
)}
<Link href={`/validator?address=${validator.operatorAddress}`}>
<div>
<b>{validator.description.moniker}</b>
</div>
<div>{validator.operatorAddress}</div>
</Link>
{votingPowerPerc && <div>Voting power: {votingPowerPercStr}</div>}
<div>
<Button disabled={disabled} onClick={onStake} structure="naked">
Stake here
</Button>
</div>
{website && (
<div>
<Link href={website} target="_blank">
{website}
</Link>
</div>
)}
</div>
);
};

export default memo(ValidatorRow);
12 changes: 7 additions & 5 deletions src/features/staking/context/actions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { StakeAddresses } from "../lib/core/base";
import {
claimRewards,
getBalance,
getDelegations,
getPool,
getRewards,
getUnbondingDelegations,
getValidatorDetails,
getValidatorsList,
setRedelegate,
stakeAmount,
unstakeAmount,
} from "../lib/core/base";
import type { AbstraxionSigningClient } from "../lib/core/client";
import { sumAllCoins } from "../lib/core/coins";
import type { StakeAddresses } from "../lib/core/tx";
import {
claimRewards,
setRedelegate,
stakeAmount,
unstakeAmount,
} from "../lib/core/tx";
import {
addDelegations,
addUnbondings,
Expand Down
12 changes: 12 additions & 0 deletions src/features/staking/context/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export const getTotalDelegation = (state: StakingState) => {
return sumAllCoins(delegationCoins);
};

export const getTotalUnbonding = (state: StakingState) => {
const { unbondings } = state;

if (!unbondings?.items.length) {
return null;
}

const unbondingCoins = unbondings.items.map((d) => d.balance);

return sumAllCoins(unbondingCoins);
};

export const getVotingPowerPerc = (
validatorTokens: string,
state: StakingState,
Expand Down
7 changes: 7 additions & 0 deletions src/features/staking/lib/core/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { chainId } from "./constants";

export const getAccountExplorerLink = (address: string) =>
`https://explorer.burnt.com/${chainId}/account/${address}`;

export const getValidatorExplorerLink = (address: string) =>
`https://explorer.burnt.com/${chainId}/staking/${address}`;
148 changes: 3 additions & 145 deletions src/features/staking/lib/core/base.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import { StargateClient } from "@cosmjs/stargate";
import type {
Coin,
DeliverTxResponse,
MsgBeginRedelegateEncodeObject,
MsgDelegateEncodeObject,
MsgUndelegateEncodeObject,
MsgWithdrawDelegatorRewardEncodeObject,
} from "@cosmjs/stargate";
import BigNumber from "bignumber.js";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import type { QueryValidatorsResponse } from "cosmjs-types/cosmos/staking/v1beta1/query";
import type {
Pool,
Validator,
} from "cosmjs-types/cosmos/staking/v1beta1/staking";
import {
MsgBeginRedelegate,
MsgDelegate,
MsgUndelegate,
} from "cosmjs-types/cosmos/staking/v1beta1/tx";

import type { AbstraxionSigningClient } from "./client";
import { getStakingQueryClient } from "./client";
import { normaliseCoin } from "./coins";
import { rpcEndpoint } from "./constants";
import { getCosmosFee } from "./fee";

let validatorsRequest: null | Promise<QueryValidatorsResponse> = null;

Expand Down Expand Up @@ -54,7 +38,9 @@ export const getValidatorDetails = async (address: string) => {
const queryClient = await getStakingQueryClient();

const promise = queryClient.staking.validator(address).then((resp) => {
validatorDetailsRequest = null;
if (validatorDetailsRequest?.[0] === address) {
validatorDetailsRequest = null;
}

return resp.validator;
});
Expand Down Expand Up @@ -89,26 +75,6 @@ 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();

Expand Down Expand Up @@ -153,111 +119,3 @@ export const getRewards = async (address: string, validatorAddress: string) => {
}))
.map((r) => normaliseCoin(r));
};

export type StakeAddresses = {
delegator: string;
validator: string;
};

export const stakeAmount = async (
addresses: StakeAddresses,
client: NonNullable<AbstraxionSigningClient>,
amount: Coin,
) => {
const msg = MsgDelegate.fromPartial({
amount,
delegatorAddress: addresses.delegator,
validatorAddress: addresses.validator,
});

const messageWrapper: MsgDelegateEncodeObject = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: msg,
};

const fee = await getCosmosFee({
address: addresses.delegator,
msgs: [messageWrapper],
});

return await client
.signAndBroadcast(addresses.delegator, [messageWrapper], fee)
.then(getTxVerifier("delegate"))
.catch(handleTxError);
};

export const unstakeAmount = async (
addresses: StakeAddresses,
client: NonNullable<AbstraxionSigningClient>,
amount: Coin,
) => {
const msg = MsgUndelegate.fromPartial({
amount,
delegatorAddress: addresses.delegator,
validatorAddress: addresses.validator,
});

const messageWrapper: MsgUndelegateEncodeObject = {
typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate",
value: msg,
};

const fee = await getCosmosFee({
address: addresses.delegator,
msgs: [messageWrapper],
});

return await client
.signAndBroadcast(addresses.delegator, [messageWrapper], fee)
.then(getTxVerifier("unbond"))
.catch(handleTxError);
};

export const claimRewards = async (
addresses: StakeAddresses,
client: NonNullable<AbstraxionSigningClient>,
) => {
const msg = MsgWithdrawDelegatorReward.fromPartial({
delegatorAddress: addresses.delegator,
validatorAddress: addresses.validator,
});

const messageWrapper = [
{
typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
value: msg,
} satisfies MsgWithdrawDelegatorRewardEncodeObject,
];

const fee = await getCosmosFee({
address: addresses.delegator,
msgs: messageWrapper,
});

return await client
.signAndBroadcast(addresses.delegator, messageWrapper, fee)
.then(getTxVerifier("withdraw_rewards"))
.catch(handleTxError);
};

// @TODO: Pass the target delegator
export const setRedelegate = async (
delegatorAddress: string,
client: NonNullable<AbstraxionSigningClient>,
) => {
const msg = MsgBeginRedelegate.fromPartial({
delegatorAddress,
});

const messageWrapper: MsgBeginRedelegateEncodeObject = {
typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate",
value: msg,
};

const fee = await getCosmosFee({
address: delegatorAddress,
msgs: [messageWrapper],
});

return await client.signAndBroadcast(delegatorAddress, [messageWrapper], fee);
};
Loading

0 comments on commit 693f842

Please sign in to comment.