Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add undelegate, redelegate and claim rewards #95

Merged
merged 11 commits into from
Apr 13, 2023
23 changes: 17 additions & 6 deletions components/dataViews/TransactionInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React from "react";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";

import { DbTransaction } from "../../types";
import { useAppContext } from "../../context/AppContext";
import HashView from "./HashView";
import { printableCoin, printableCoins } from "../../lib/displayHelpers";
import { DbTransaction } from "../../types";
import StackableContainer from "../layout/StackableContainer";
import { printableCoins, printableCoin } from "../../lib/displayHelpers";
import HashView from "./HashView";

interface Props {
tx: DbTransaction;
Expand All @@ -32,7 +30,7 @@ const TransactionInfo = (props: Props) => {
</li>
</>
) : msg.typeUrl === "/cosmos.staking.v1beta1.MsgDelegate" ||
msg.typeUrl === "/cosmos.staking.v1beta1.MsgUnDelegate" ? (
msg.typeUrl === "/cosmos.staking.v1beta1.MsgUndelegate" ? (
<>
<li>
<label>Amount:</label>
Expand Down Expand Up @@ -64,6 +62,19 @@ const TransactionInfo = (props: Props) => {
</div>
</li>
</>
) : msg.typeUrl === "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward" ? (
<>
<li>
<label>Amount:</label>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<div>{printableCoin(props.tx.msgs[0].value.amount, state.chain)}</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same issue for the whole file, but we can do

Suggested change
<div>{printableCoin(props.tx.msgs[0].value.amount, state.chain)}</div>
<div>{printableCoin(msg.value.amount, state.chain)}</div>

since we loop over messages already. This makes the code future proof for multi message transactions (which we soon need to withdraw rewards from multiple validators).

</li>
<li>
<label>Validator Address:</label>
<div title={props.tx.msgs[0].value.validatorAddress}>
<HashView hash={props.tx.msgs[0].value.validatorAddress} />
</div>
</li>
</>
) : null,
)}
{props.tx.fee && (
Expand Down
185 changes: 185 additions & 0 deletions components/forms/ReDelegationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { Decimal } from "@cosmjs/math";
import { Account, calculateFee } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import axios from "axios";
import { NextRouter, withRouter } from "next/router";
import { useState } from "react";
import { useAppContext } from "../../context/AppContext";
import { checkAddress, exampleValidatorAddress } from "../../lib/displayHelpers";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";

interface Props {
address: string | null;
accountOnChain: Account | null;
router: NextRouter;
closeForm: () => void;
}

const ReDelegationForm = (props: Props) => {
const { state } = useAppContext();
const [validatorSrcAddress, setValidatorSrcAddress] = useState("");
const [validatorDstAddress, setValidatorDstAddress] = useState("");
const [amount, setAmount] = useState("0");
const [memo, setMemo] = useState("");
const [gas, setGas] = useState(300000);
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
const [_processing, setProcessing] = useState(false);
const [addressError, setAddressError] = useState("");

const createTransaction = (
txValidatorSrcAddress: string,
txValidatorDstAddress: string,
txAmount: string,
gasLimit: number,
) => {
assert(Number.isSafeInteger(gasLimit) && gasLimit > 0, "gas limit must be a positive integer");

const amountInAtomics = Decimal.fromUserInput(
txAmount,
Number(state.chain.displayDenomExponent),
).atomics;
const msgRedelegate = {
delegatorAddress: props.address,
validatorSrcAddress: txValidatorSrcAddress,
validatorDstAddress: txValidatorDstAddress,
amount: {
amount: amountInAtomics,
denom: state.chain.denom,
},
};
const msg = {
typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate",
value: msgRedelegate,
};
assert(gasPrice, "gasPrice missing");
const fee = calculateFee(gasLimit, gasPrice);
const { accountOnChain } = props;
assert(accountOnChain, "accountOnChain missing");
return {
accountNumber: accountOnChain.accountNumber,
sequence: accountOnChain.sequence,
chainId: state.chain.chainId,
msgs: [msg],
fee: fee,
memo: memo,
};
};

const handleCreate = async () => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const validatorSrcAddressError = checkAddress(validatorSrcAddress, state.chain.addressPrefix);
const validatorDstAddressError = checkAddress(validatorDstAddress, state.chain.addressPrefix);
if (validatorSrcAddressError) {
setAddressError(
`Invalid address for network ${state.chain.chainId}: ${validatorSrcAddressError}`,
);
return;
}
if (validatorDstAddressError) {
setAddressError(
`Invalid address for network ${state.chain.chainId}: ${validatorDstAddressError}`,
);
return;
}

setProcessing(true);
const tx = createTransaction(validatorSrcAddress, validatorDstAddress, amount, gas);
console.log(tx, "tx data");
const dataJSON = JSON.stringify(tx);
const res = await axios.post("/api/transaction", { dataJSON });
console.log(dataJSON, "tx dataJSON", res);
const { transactionID } = res.data;
props.router.push(`${props.address}/transaction/${transactionID}`);
};

assert(state.chain.addressPrefix, "addressPrefix missing");

return (
<StackableContainer lessPadding>
<button className="remove" onClick={() => props.closeForm()}>
</button>
<h2>Create ReDelegation</h2>
<div className="form-item">
<Input
label="Validator Source Address"
name="validatorSourceAddress"
value={validatorSrcAddress}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValidatorSrcAddress(e.target.value)
}
error={addressError}
placeholder={`E.g. ${exampleValidatorAddress(0, state.chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label="Validator Destination Address"
name="validatorDestinationAddress"
value={validatorDstAddress}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValidatorDstAddress(e.target.value)
}
error={addressError}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need separate error props, one for each field?

placeholder={`E.g. ${exampleValidatorAddress(1, state.chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label={`Amount (${state.chain.displayDenom})`}
name="amount"
type="number"
value={amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAmount(e.target.value)}
/>
</div>
<div className="form-item">
<Input
label="Gas Limit"
name="gas"
type="number"
value={gas}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setGas(parseInt(e.target.value, 10))
}
/>
</div>
<div className="form-item">
<Input label="Gas Price" name="gas_price" type="string" value={gasPrice} disabled={true} />
</div>
<div className="form-item">
<Input
label="Memo"
name="memo"
type="text"
value={memo}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
/>
</div>
<Button label="ReDelegate" onClick={handleCreate} />
<style jsx>{`
p {
margin-top: 15px;
}
.form-item {
margin-top: 1.5em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};

export default withRouter(ReDelegationForm);
138 changes: 138 additions & 0 deletions components/forms/RewardsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Account, calculateFee } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import axios from "axios";
import { NextRouter, withRouter } from "next/router";
import { useState } from "react";
import { useAppContext } from "../../context/AppContext";
import { checkAddress, exampleValidatorAddress } from "../../lib/displayHelpers";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";

interface Props {
address: string | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to make explicit which address this is. For this component we can say delegatorAddress which is then set to the multisig address that signs the transaction.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this prop should not be nullable because otherwise we don't create the transaction correctly.

accountOnChain: Account | null;
router: NextRouter;
closeForm: () => void;
}

const RewardsForm = (props: Props) => {
const { state } = useAppContext();
const [validatorAddress, setValidatorAddress] = useState("");
const [memo, setMemo] = useState("");
const [gas, setGas] = useState(200000);
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
const [_processing, setProcessing] = useState(false);
const [addressError, setAddressError] = useState("");

const createTransaction = (txValidatorAddress: string, gasLimit: number) => {
assert(Number.isSafeInteger(gasLimit) && gasLimit > 0, "gas limit must be a positive integer");

const msgDelegatorReward = {
delegatorAddress: props.address,
validatorAddress: txValidatorAddress,
};
const msg = {
typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
value: msgDelegatorReward,
};
assert(gasPrice, "gasPrice missing");
const fee = calculateFee(gasLimit, gasPrice);
const { accountOnChain } = props;
assert(accountOnChain, "accountOnChain missing");
return {
accountNumber: accountOnChain.accountNumber,
sequence: accountOnChain.sequence,
chainId: state.chain.chainId,
msgs: [msg],
fee: fee,
memo: memo,
};
};

const handleCreate = async () => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const validatorAddressError = checkAddress(validatorAddress, state.chain.addressPrefix);
if (validatorAddressError) {
setAddressError(
`Invalid address for network ${state.chain.chainId}: ${validatorAddressError}`,
);
return;
}

setProcessing(true);
const tx = createTransaction(validatorAddress, gas);
console.log(tx, "tx data");
const dataJSON = JSON.stringify(tx);
const res = await axios.post("/api/transaction", { dataJSON });
console.log(dataJSON, "tx dataJSON", res);
const { transactionID } = res.data;
props.router.push(`${props.address}/transaction/${transactionID}`);
};

assert(state.chain.addressPrefix, "addressPrefix missing");

return (
<StackableContainer lessPadding>
<button className="remove" onClick={() => props.closeForm()}>
</button>
<h2>Claim Rewards</h2>
<div className="form-item">
<Input
label="Validator Address"
name="validatorAddress"
value={validatorAddress}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValidatorAddress(e.target.value)}
error={addressError}
placeholder={`E.g. ${exampleValidatorAddress(0, state.chain.addressPrefix)})}`}
/>
</div>
<div className="form-item">
<Input
label="Gas Limit"
name="gas"
type="number"
value={gas}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setGas(parseInt(e.target.value, 10))
}
/>
</div>
<div className="form-item">
<Input label="Gas Price" name="gas_price" type="string" value={gasPrice} disabled={true} />
</div>
<div className="form-item">
<Input
label="Memo"
name="memo"
type="text"
value={memo}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
/>
</div>
<Button label="Claim Rewards" onClick={handleCreate} />
<style jsx>{`
p {
margin-top: 15px;
}
.form-item {
margin-top: 1.5em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};

export default withRouter(RewardsForm);
Loading