Skip to content

Commit

Permalink
anoma#86 staking and unstaking (anoma#87)
Browse files Browse the repository at this point in the history
* changed all fake data to go through actions
* cleaned up staking-related types
* Added links to the UI descriptions to the local readme of the state
* removed empty reducers
* added staking position listing to validator details
* added a comment about the redundancy of actions
  • Loading branch information
memasdeligeorgakis authored Oct 17, 2022
1 parent 0b1360f commit f057a4f
Show file tree
Hide file tree
Showing 20 changed files with 580 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import styled from "styled-components/macro";

export const BondingPositionContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: start;
align-items: center;
width: 100%;
margin: 16px 0 16px;
overflow-y: scroll;
color: ${(props) => props.theme.colors.utility2.main};
`;

export const BondingAmountInputContainer = styled.div`
display: flex;
width: 100%;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useState } from "react";
import {
BondingPositionContainer,
BondingAmountInputContainer,
} from "./NewBondingPosition.components";
import { Table, TableConfigurations, KeyValueData } from "components/Table";
import { Button, ButtonVariant } from "components/Button";
import {
StakingPosition,
ChangeInStakingPosition,
} from "slices/StakingAndGovernance";

const REMAINS_BONDED_KEY = "Remains bonded";

// configuration for the summary table that represents all the data in this view
const bondingDetailsConfigurations: TableConfigurations<KeyValueData, never> = {
rowRenderer: (rowData: KeyValueData) => {
// if this is the last row we style it bold
const styleForRemainsBondedRow =
rowData.key === REMAINS_BONDED_KEY ? { fontWeight: "bold" } : {};
return (
<>
<td style={{ display: "flex", ...styleForRemainsBondedRow }}>
{rowData.key}
</td>
<td style={styleForRemainsBondedRow}>{rowData.value}</td>
</>
);
},
columns: [
{ uuid: "1", columnLabel: "", width: "30%" },
{ uuid: "2", columnLabel: "", width: "70%" },
],
};

type Props = {
currentBondingPosition: StakingPosition;
// this is how much we have available for bonding
totalFundsToBond: number;
// called when the user confirms bonding
confirmBonding: (changeInStakingPosition: ChangeInStakingPosition) => void;
// called when the user cancels bonding
cancelBonding: () => void;
};

// contains everything what the user needs for bonding funds
export const NewBondingPosition = (props: Props): JSX.Element => {
const {
currentBondingPosition,
totalFundsToBond,
confirmBonding,
cancelBonding,
} = props;
const { validatorId, stakedCurrency } = currentBondingPosition;

// storing the unbonding input value locally here as string
// we threat them as strings except below in validation
// might have to change later to numbers
const [amountToBond, setAmountToBond] = useState("");

// unbonding amount and displayed value with a very naive validation
// TODO (https://github.com/anoma/namada-interface/issues/4#issuecomment-1260564499)
// do proper validation as part of input
const bondedAmountAsNumber = Number(currentBondingPosition.stakedAmount);
const amountToBondNumber = Number(amountToBond);

// if this is the case, we display error message
const isEntryIncorrect =
(amountToBond !== "" && amountToBondNumber <= 0) ||
amountToBondNumber > totalFundsToBond ||
Number.isNaN(amountToBondNumber);

// if incorrect or empty value we disable the button
const isEntryIncorrectOrEmpty = isEntryIncorrect || amountToBond === "";

// we convey this with an object that can be used
const remainsBondedToDisplay = isEntryIncorrect
? `The bonding amount can be more than 0 and at most ${totalFundsToBond}`
: `${bondedAmountAsNumber + amountToBondNumber}`;

// data for the table
const bondingSummary = [
{
uuid: "1",
key: "Total Funds",
value: `${totalFundsToBond}`,
},
{
uuid: "2",
key: "Bonded amount",
value: currentBondingPosition.stakedAmount,
},
{
uuid: "3",
key: "Amount to bond",
value: amountToBond,
hint: "stake",
},
{
uuid: "4",
key: REMAINS_BONDED_KEY,
value: remainsBondedToDisplay,
},
];

return (
<BondingPositionContainer>
{/* input field */}
<BondingAmountInputContainer>
<input
onChange={(event) => {
setAmountToBond(event.target.value);
}}
/>
</BondingAmountInputContainer>

{/* summary table */}
<Table
title="Summary"
tableConfigurations={bondingDetailsConfigurations}
data={bondingSummary}
/>

{/* confirmation and cancel */}
<Button
variant={ButtonVariant.Contained}
onClick={() => {
const changeInStakingPosition: ChangeInStakingPosition = {
amount: amountToBond,
stakingCurrency: stakedCurrency,
validatorId: validatorId,
};
confirmBonding(changeInStakingPosition);
}}
disabled={isEntryIncorrectOrEmpty}
>
Confirm
</Button>
<Button
variant={ButtonVariant.Contained}
onClick={() => {
cancelBonding();
}}
style={{ backgroundColor: "lightgrey", color: "black" }}
>
Cancel
</Button>
</BondingPositionContainer>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NewBondingPosition } from "./NewBondingPosition";
94 changes: 90 additions & 4 deletions apps/namada-interface/src/App/Staking/Staking.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { Routes, Route, useNavigate, useLocation } from "react-router-dom";
import { MainContainerNavigation } from "App/StakingAndGovernance/MainContainerNavigation";
import { Modal } from "components/Modal";
import { StakingContainer } from "./Staking.components";
import { StakingOverview } from "./StakingOverview";
import { ValidatorDetails } from "./ValidatorDetails";
Expand All @@ -10,7 +11,10 @@ import {
Validator,
MyValidators,
StakingPosition,
ChangeInStakingPosition,
} from "slices/StakingAndGovernance";
import { NewBondingPosition } from "./NewBondingPosition";
import { UnbondPosition } from "./UnbondPosition";

const initialTitle = "Staking";

Expand Down Expand Up @@ -48,18 +52,45 @@ type Props = {
myValidators: MyValidators[];
myStakingPositions: StakingPosition[];
selectedValidatorId: string | undefined;
onInitCallback: () => void; // will be called at first load, triggers fetching
// will be called at first load, parent decides what happens
onInitCallback: () => void;
fetchValidatorDetails: (validatorId: string) => void;
postNewBonding: (changeInStakingPosition: ChangeInStakingPosition) => void;
postNewUnbonding: (changeInStakingPosition: ChangeInStakingPosition) => void;
};

// in this view we can be in one of these states at any given time
export enum ModalState {
None,
NewBonding,
Unbond,
}

// TODO: these should go to Modal component
export enum ModalOnRequestCloseType {
Confirm,
Cancel,
}

// This is the parent view for all staking related views. Most of the
// staking specific functions are defined here and passed down as props.
// This contains the main vies in staking:
// * StakingOverview - displaying an overview of the users staking and validators
// * ValidatorDetails - as the name says
// * NewStakingStakingPosition - rendered in modal on top of other content
// this is for creating new staking positions
// * UnstakePositions - rendered in modal on top of other content, for unstaking
export const Staking = (props: Props): JSX.Element => {
const [breadcrumb, setBreadcrumb] = useState([initialTitle]);
const [modalState, setModalState] = useState(ModalState.None);
const location = useLocation();
const navigate = useNavigate();

const {
onInitCallback,
fetchValidatorDetails,
postNewBonding,
postNewUnbonding,
myBalances,
validators,
myValidators,
Expand All @@ -68,9 +99,10 @@ export const Staking = (props: Props): JSX.Element => {
} = props;

// these 2 are needed for validator details
const stakingPositionsWithSelectedValidator = myStakingPositions.filter(
(validator) => validator.validatorId === selectedValidatorId
);
const stakingPositionsWithSelectedValidator =
myStakingPositions.filter(
(validator) => validator.validatorId === selectedValidatorId
);

const selectedValidator = validators.find(
(validator) => validator.uuid === selectedValidatorId
Expand Down Expand Up @@ -113,6 +145,29 @@ export const Staking = (props: Props): JSX.Element => {
fetchValidatorDetails(validatorId);
};

// callbacks for the bonding and unbonding views
const confirmBonding = (
changeInStakingPosition: ChangeInStakingPosition
): void => {
setModalState(ModalState.None);
postNewBonding(changeInStakingPosition);
};

const cancelBonding = (): void => {
setModalState(ModalState.None);
};

const confirmUnbonding = (
changeInStakingPosition: ChangeInStakingPosition
): void => {
setModalState(ModalState.None);
postNewUnbonding(changeInStakingPosition);
};

const cancelUnbonding = (): void => {
setModalState(ModalState.None);
};

return (
<StakingContainer>
<MainContainerNavigation
Expand All @@ -124,6 +179,36 @@ export const Staking = (props: Props): JSX.Element => {
setBreadcrumb([initialTitle]);
}}
/>
{/* modal for bonding */}
<Modal
isOpen={modalState === ModalState.NewBonding}
title={`Stake with ${selectedValidator?.name}`}
onBackdropClick={() => {
cancelBonding();
}}
>
<NewBondingPosition
totalFundsToBond={100}
confirmBonding={confirmBonding}
cancelBonding={cancelBonding}
currentBondingPosition={stakingPositionsWithSelectedValidator[0]}
/>
</Modal>

{/* modal for unbonding */}
<Modal
isOpen={modalState === ModalState.Unbond}
title="Unstake"
onBackdropClick={() => {
cancelUnbonding();
}}
>
<UnbondPosition
confirmUnbonding={confirmUnbonding}
cancelUnbonding={cancelUnbonding}
currentBondingPosition={stakingPositionsWithSelectedValidator[0]}
/>
</Modal>
<Routes>
<Route
path={StakingAndGovernanceSubRoute.StakingOverview}
Expand All @@ -144,6 +229,7 @@ export const Staking = (props: Props): JSX.Element => {
stakingPositionsWithSelectedValidator={
stakingPositionsWithSelectedValidator
}
setModalState={setModalState}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { StakingOverviewContainer } from "./StakingOverview.components";
import {
Table,
TableLink,
Expand All @@ -10,6 +9,7 @@ import {
Validator,
MyValidators,
} from "slices/StakingAndGovernance";
import { StakingOverviewContainer } from "./StakingOverview.components";

// My Balances table row renderer and configuration
const myBalancesRowRenderer = (myBalanceEntry: MyBalanceEntry): JSX.Element => {
Expand Down Expand Up @@ -135,6 +135,12 @@ type Props = {
myValidators: MyValidators[];
};

// This is the default view for the staking. it displays all the relevant
// staking information of the user and allows unstake the active staking
// positions directly from here.
// * Unstaking happens by calling a callback that triggers a modal
// view in the parent
// * user can also navigate to sibling view for validator details
export const StakingOverview = (props: Props): JSX.Element => {
const { navigateToValidatorDetails, myBalances, validators, myValidators } =
props;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import styled from "styled-components/macro";

export const UnstakePositionContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: start;
align-items: center;
width: 100%;
margin: 16px 0 16px;
overflow-y: scroll;
color: ${(props) => props.theme.colors.utility2.main};
`;
Loading

0 comments on commit f057a4f

Please sign in to comment.