Skip to content

Commit

Permalink
feat: fix up staking
Browse files Browse the repository at this point in the history
  - Make ChangeInStakingPosition amount a BigNumber instead of string

  - ChangeInStakingPosition is now always positive (negative does not
    represent unbond)

  - Make StakingPosition stakedAmount a BigNumber instead of string

  - Remove myValidators prop from Staking, StakingOverview,
    MyValidatorsTable; use redux selector instead

  - Split StakingOverview into AllValidatorsTable, MyValidatorsTable,
    StakingBalancesList

  - Make stakedAmount in UnbondPosition BigNumber instead of number

  - Do not negate unbond amount in UnbondPosition; just pass amount as
    positive value

  - Display http URLs as links in ValidatorDetails

  - Display amounts that haven't fetched yet as "-" instead of "NAM 0"
    (where?)
    - ValidatorDetails

  - Store validator commission as a BigNumber instead of a string

  - Make stake returned from validator optional instead of 0 if None in
    query

  - Add unbonded and withdrawable amounts to all validators query

  - Add toasts to submit bond and submit unbond actions

  - Make myValidators optional to allow to be undefined when not fetched

  - Make votingPower optional in case where None returned from query

  - Make stakedAmount, unbondedAmount, withdrawableAmount optional for
    same reasons

  - Add subheading slot to Table

  - Add filtering by search and sorting by column to all validators
    table
  • Loading branch information
emccorson committed Jul 13, 2023
1 parent 9f52b71 commit a8af172
Show file tree
Hide file tree
Showing 24 changed files with 590 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export const NewBondingPosition = (props: Props): JSX.Element => {
variant={ButtonVariant.Contained}
onClick={() => {
const changeInStakingPosition: ChangeInStakingPosition = {
amount: amountToBond,
amount: amountToBondNumber,
owner: currentAddress,
validatorId: currentBondingPositions[0].validatorId,
};
Expand Down
6 changes: 2 additions & 4 deletions apps/namada-interface/src/App/Staking/Staking.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { Routes, Route, useNavigate } from "react-router-dom";
import BigNumber from "bignumber.js";

import { truncateInMiddle } from "@anoma/utils";
import { Modal } from "@anoma/components";
Expand Down Expand Up @@ -53,7 +54,7 @@ const validatorNameFromUrl = (path: string): string | undefined => {
const emptyStakingPosition = (validatorId: string): StakingPosition => ({
uuid: validatorId,
stakingStatus: "",
stakedAmount: "",
stakedAmount: new BigNumber(0),
owner: "",
totalRewards: "",
validatorId: validatorId,
Expand All @@ -62,7 +63,6 @@ const emptyStakingPosition = (validatorId: string): StakingPosition => ({
type Props = {
accounts: Account[];
validators: Validator[];
myValidators: MyValidators[];
myStakingPositions: StakingPosition[];
selectedValidatorId: string | undefined;
// will be called at first load, parent decides what happens
Expand Down Expand Up @@ -106,7 +106,6 @@ export const Staking = (props: Props): JSX.Element => {
postNewBonding,
postNewUnbonding,
validators,
myValidators,
myStakingPositions,
selectedValidatorId,
} = props;
Expand Down Expand Up @@ -228,7 +227,6 @@ export const Staking = (props: Props): JSX.Element => {
element={
<StakingOverview
navigateToValidatorDetails={navigateToValidatorDetails}
myValidators={myValidators}
validators={validators}
accounts={accounts}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from "styled-components";

export const AllValidatorsSearchBar = styled.div`
margin-bottom: 8px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { useState } from "react";
import BigNumber from "bignumber.js";
import { Table, TableLink, TableConfigurations } from "@anoma/components";
import { Account } from "slices/accounts";
import { Validator, MyValidators } from "slices/StakingAndGovernance";
import {
AllValidatorsSearchBar
} from "./AllValidatorsTable.components";
import { formatPercentage, assertNever } from "@anoma/utils";
import { useAppSelector, RootState } from "store";
import { ValidatorsCallbacks } from "../StakingOverview";

// AllValidators table row renderer and configuration
// it contains callbacks defined in AllValidatorsCallbacks
const AllValidatorsRowRenderer = (
validator: Validator,
callbacks?: ValidatorsCallbacks
): JSX.Element => {
return (
<>
<td>
<TableLink
onClick={() => {
const formattedValidatorName = validator.name
.replace(" ", "-")
.toLowerCase();

callbacks && callbacks.onClickValidator(formattedValidatorName);
}}
>
{validator.name}
</TableLink>
</td>
<td>{validator.votingPower?.toString() ?? ""}</td>
<td>{formatPercentage(validator.commission)}</td>
</>
);
};

const getAllValidatorsConfiguration = (
navigateToValidatorDetails: (validatorId: string) => void,
onColumnClick: (column: AllValidatorsColumn) => void,
sort: Sort,
): TableConfigurations<Validator, ValidatorsCallbacks> => {
const getLabelWithTriangle = (column: AllValidatorsColumn): string => {
let triangle = "";
if (sort.column === column) {
if (sort.ascending) {
triangle = " \u25b5"; // white up-pointing small triangle
} else {
triangle = " \u25bf"; // white down-pointing small triangle
}
}

return `${column}${triangle}`;
}

return {
rowRenderer: AllValidatorsRowRenderer,
callbacks: {
onClickValidator: navigateToValidatorDetails,
},
columns: [
{
uuid: "1",
columnLabel: getLabelWithTriangle(AllValidatorsColumn.Validator),
width: "45%",
onClick: () => onColumnClick(AllValidatorsColumn.Validator),
},
{
uuid: "2",
columnLabel: getLabelWithTriangle(AllValidatorsColumn.VotingPower),
width: "25%",
onClick: () => onColumnClick(AllValidatorsColumn.VotingPower),
},
{
uuid: "3",
columnLabel: getLabelWithTriangle(AllValidatorsColumn.Commission),
width: "30%",
onClick: () => onColumnClick(AllValidatorsColumn.Commission),
},
],
};
};

const sortValidators = (sort: Sort, validators: Validator[]): Validator[] => {
const direction = sort.ascending ? 1 : -1;

const ascendingSortFn: (a: Validator, b: Validator) => number =
sort.column === AllValidatorsColumn.Validator ?
(a, b) => a.name.localeCompare(b.name) :
sort.column === AllValidatorsColumn.VotingPower ?
((a, b) =>
a.votingPower === undefined || b.votingPower === undefined ? 0 :
a.votingPower.isLessThan(b.votingPower) ? -1 : 1) :
sort.column === AllValidatorsColumn.Commission ?
((a, b) => a.commission.isLessThan(b.commission) ? -1 : 1) :
assertNever(sort.column);

const cloned = validators.slice();
cloned.sort((a, b) => direction * ascendingSortFn(a, b));

return cloned;
}

const filterValidators = (search: string, validators: Validator[]): Validator[] =>
validators.filter(v =>
search === ""
? true
: v.name.toLowerCase().startsWith(search.toLowerCase())
);

const selectSortedFilteredValidators = (sort: Sort, search: string) =>
(state: RootState): Validator[] => {
const validators = state.stakingAndGovernance.validators;

const sorted = sortValidators(sort, validators);
const sortedAndFiltered = filterValidators(search, sorted);

return sortedAndFiltered;
};

enum AllValidatorsColumn {
Validator = "Validator",
VotingPower = "Voting power",
Commission = "Commission"
};

type Sort = {
column: AllValidatorsColumn;
ascending: boolean;
};

export const AllValidatorsTable: React.FC<{
navigateToValidatorDetails: (validatorId: string) => void;
}> = ({
navigateToValidatorDetails,
}) => {
const [search, setSearch] = useState("");

const [sort, setSort] = useState<Sort>({
column: AllValidatorsColumn.Validator,
ascending: true
});

const sortedFilteredValidators = useAppSelector(
selectSortedFilteredValidators(sort, search)
);

const handleColumnClick = (column: AllValidatorsColumn): void =>
setSort({
column,
ascending: sort.column === column ? !sort.ascending : true
});

const allValidatorsConfiguration = getAllValidatorsConfiguration(
navigateToValidatorDetails,
handleColumnClick,
sort
);

return (
<Table
title="All Validators"
subheadingSlot={
<AllValidatorsSearchBar>
<input
type="search"
placeholder="Validator"
value={search}
onChange={e => setSearch(e.target.value)}
>
</input>
</AllValidatorsSearchBar>
}
data={sortedFilteredValidators}
tableConfigurations={allValidatorsConfiguration}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AllValidatorsTable } from "./AllValidatorsTable";
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from "styled-components";

export const AllValidatorsSearchBar = styled.div`
margin-bottom: 8px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from "react";
import BigNumber from "bignumber.js";
import { Table, TableLink, TableConfigurations } from "@anoma/components";
import { Account } from "slices/accounts";
import { Validator, MyValidators, StakingAndGovernanceState } from "slices/StakingAndGovernance";
import { formatPercentage, assertNever, showMaybeNam } from "@anoma/utils";
import { useAppSelector, RootState } from "store";
import { ValidatorsCallbacks } from "../StakingOverview";

import * as fake from "slices/StakingAndGovernance/fakeData";

const MyValidatorsRowRenderer = (
myValidatorRow: MyValidators,
callbacks?: ValidatorsCallbacks
): JSX.Element => {
return (
<>
<td>
<TableLink
onClick={() => {
const formattedValidatorName = myValidatorRow.validator.name
.replace(" ", "-")
.toLowerCase();

// this function is defined at <Staking />
// there it triggers a navigation. It then calls a callback
// that was passed to it by its' parent <StakingAndGovernance />
// in that callback function that is defined in <StakingAndGovernance />
// an action is dispatched to fetch validator data and make in available
callbacks && callbacks.onClickValidator(formattedValidatorName);
}}
>
{myValidatorRow.validator.name}
</TableLink>
</td>
<td>{myValidatorRow.stakingStatus}</td>
<td>{showMaybeNam(myValidatorRow.stakedAmount)}</td>
<td>{showMaybeNam(myValidatorRow.unbondedAmount)}</td>
<td>{showMaybeNam(myValidatorRow.withdrawableAmount)}</td>
</>
);
};

const getMyValidatorsConfiguration = (
navigateToValidatorDetails: (validatorId: string) => void
): TableConfigurations<MyValidators, ValidatorsCallbacks> => {
return {
rowRenderer: MyValidatorsRowRenderer,
columns: [
{ uuid: "1", columnLabel: "Validator", width: "30%" },
{ uuid: "2", columnLabel: "Status", width: "20%" },
{ uuid: "3", columnLabel: "Staked Amount", width: "30%" },
{ uuid: "4", columnLabel: "Unbonded Amount", width: "30%" },
{ uuid: "5", columnLabel: "Withdrawable Amount", width: "30%" },
],
callbacks: {
onClickValidator: navigateToValidatorDetails,
},
};
};

export const MyValidatorsTable: React.FC<{
navigateToValidatorDetails: (validatorId: string) => void;
}> = ({
navigateToValidatorDetails,
}) => {
const stakingAndGovernanceState = useAppSelector<StakingAndGovernanceState>(
state => state.stakingAndGovernance);
const myValidators = stakingAndGovernanceState.myValidators ?? [];

const myValidatorsConfiguration = getMyValidatorsConfiguration(
navigateToValidatorDetails
);

return (
<Table
title="My Validators"
data={myValidators}
tableConfigurations={myValidatorsConfiguration}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MyValidatorsTable } from "./MyValidatorsTable";
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from "styled-components";

export const StakingBalances = styled.div`
width: 100%;
display: grid;
grid-template-columns: minmax(150px, 30%) 1fr;
`;
export const StakingBalancesLabel = styled.div``;

export const StakingBalancesValue = styled.div``;
Loading

0 comments on commit a8af172

Please sign in to comment.