Merge pull request #118 from cosmos/feat/support-n-msgs

Support list of msgs
This commit is contained in:
Abel Fernández
2023-05-25 15:13:22 +02:00
committed by GitHub
13 changed files with 688 additions and 358 deletions
@@ -0,0 +1,47 @@
import { TxMsgSetWithdrawAddress } from "../../../types/txMsg";
import HashView from "../HashView";
interface TxMsgSetWithdrawAddressDetailsProps {
readonly msg: TxMsgSetWithdrawAddress;
}
const TxMsgSetWithdrawAddressDetails = ({ msg }: TxMsgSetWithdrawAddressDetailsProps) => (
<>
<li>
<h2>MsgSetWithdrawAddress</h2>
</li>
<li>
<label>Withdraw Address:</label>
<div title={msg.value.withdrawAddress}>
<HashView hash={msg.value.withdrawAddress} />
</div>
</li>
<style jsx>{`
li:not(:has(h2)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
`}</style>
</>
);
export default TxMsgSetWithdrawAddressDetails;
+32 -26
View File
@@ -5,6 +5,7 @@ import {
isTxMsgDelegate,
isTxMsgRedelegate,
isTxMsgSend,
isTxMsgSetWithdrawAddress,
isTxMsgUndelegate,
} from "../../../lib/txMsgHelpers";
import { DbTransaction } from "../../../types";
@@ -13,6 +14,7 @@ import TxMsgClaimRewardsDetails from "./TxMsgClaimRewardsDetails";
import TxMsgDelegateDetails from "./TxMsgDelegateDetails";
import TxMsgRedelegateDetails from "./TxMsgRedelegateDetails";
import TxMsgSendDetails from "./TxMsgSendDetails";
import TxMsgSetWithdrawAddressDetails from "./TxMsgSetWithdrawAddressDetails";
import TxMsgUndelegateDetails from "./TxMsgUndelegateDetails";
interface Props {
@@ -23,43 +25,47 @@ const TransactionInfo = ({ tx }: Props) => {
const { state } = useAppContext();
return (
<StackableContainer lessPadding lessMargin>
<>
<ul className="meta-data">
<>
{tx.msgs.map((msg, index) => (
<>
{isTxMsgSend(msg) ? <TxMsgSendDetails key={index} msg={msg} /> : null}
{isTxMsgDelegate(msg) ? <TxMsgDelegateDetails key={index} msg={msg} /> : null}
{isTxMsgUndelegate(msg) ? <TxMsgUndelegateDetails key={index} msg={msg} /> : null}
{isTxMsgRedelegate(msg) ? <TxMsgRedelegateDetails key={index} msg={msg} /> : null}
{isTxMsgClaimRewards(msg) ? <TxMsgClaimRewardsDetails key={index} msg={msg} /> : null}
</>
<StackableContainer key={index} lessPadding lessMargin>
{isTxMsgSend(msg) ? <TxMsgSendDetails msg={msg} /> : null}
{isTxMsgDelegate(msg) ? <TxMsgDelegateDetails msg={msg} /> : null}
{isTxMsgUndelegate(msg) ? <TxMsgUndelegateDetails msg={msg} /> : null}
{isTxMsgRedelegate(msg) ? <TxMsgRedelegateDetails msg={msg} /> : null}
{isTxMsgClaimRewards(msg) ? <TxMsgClaimRewardsDetails msg={msg} /> : null}
{isTxMsgSetWithdrawAddress(msg) ? <TxMsgSetWithdrawAddressDetails msg={msg} /> : null}
</StackableContainer>
))}
{tx.fee ? (
<>
<StackableContainer lessPadding lessMargin>
{tx.fee ? (
<>
<li>
<label>Gas:</label>
<div>{tx.fee.gas}</div>
</li>
<li>
<label>Fee:</label>
<div>{printableCoins(tx.fee.amount, state.chain)}</div>
</li>
</>
) : null}
{tx.memo ? (
<li>
<label>Gas:</label>
<div>{tx.fee.gas}</div>
<label>Memo:</label>
<div>{tx.memo}</div>
</li>
<li>
<label>Fee:</label>
<div>{printableCoins(tx.fee.amount, state.chain)}</div>
</li>
</>
) : null}
{tx.memo ? (
<li>
<label>Memo:</label>
<div>{tx.memo}</div>
</li>
) : null}
) : null}
</StackableContainer>
</>
</ul>
<style jsx>{`
ul {
.meta-data {
list-style: none;
padding: 0;
margin: 0;
margin-top: 25px;
}
.meta-data li {
margin-top: 10px;
@@ -80,7 +86,7 @@ const TransactionInfo = ({ tx }: Props) => {
display: block;
}
`}</style>
</StackableContainer>
</>
);
};
@@ -1,47 +1,69 @@
import { assert } from "@cosmjs/utils";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgClaimRewards } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgClaimRewards } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgClaimRewardsFormProps {
readonly delegatorAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgClaimRewardsForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgClaimRewardsFormProps) => {
const MsgClaimRewardsForm = ({
delegatorAddress,
setMsgGetter,
deleteMsg,
}: MsgClaimRewardsFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
const [validatorAddress, setValidatorAddress] = useState("");
const [validatorAddressError, setValidatorAddressError] = useState("");
const checkAndGetMsg = useCallback(() => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return null;
}
const msg: TxMsgClaimRewards = {
typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
value: { delegatorAddress, validatorAddress },
};
return msg;
}, [delegatorAddress, state.chain.addressPrefix, state.chain.chainId, validatorAddress]);
useEffect(() => {
setCheckAndGetMsg(() => checkAndGetMsg);
}, [checkAndGetMsg, setCheckAndGetMsg]);
try {
setValidatorAddressError("");
const isMsgValid = (msg: TxMsg): msg is TxMsgClaimRewards => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return false;
}
return isTxMsgClaimRewards(msg);
};
const msg: TxMsgClaimRewards = {
typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward",
value: { delegatorAddress, validatorAddress },
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
delegatorAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
validatorAddress,
]);
return (
<>
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgWithdrawDelegatorReward</h2>
<div className="form-item">
<Input
label="Validator Address"
@@ -53,14 +75,22 @@ const MsgClaimRewardsForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgClaimRe
/>
</div>
<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>
);
};
@@ -1,17 +1,21 @@
import { Decimal } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgDelegate } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgDelegate } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgDelegateFormProps {
readonly delegatorAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFormProps) => {
const MsgDelegateForm = ({ delegatorAddress, setMsgGetter, deleteMsg }: MsgDelegateFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
@@ -21,41 +25,52 @@ const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFor
const [validatorAddressError, setValidatorAddressError] = useState("");
const [amountError, setAmountError] = useState("");
const checkAndGetMsg = useCallback(() => {
assert(state.chain.addressPrefix, "addressPrefix missing");
assert(state.chain.denom, "denom missing");
useEffect(() => {
try {
assert(state.chain.denom, "denom missing");
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return null;
}
setValidatorAddressError("");
setAmountError("");
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return null;
}
const isMsgValid = (msg: TxMsg): msg is TxMsgDelegate => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const amountInAtomics = Decimal.fromUserInput(
amount,
Number(state.chain.displayDenomExponent),
).atomics;
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return false;
}
const msg: TxMsgDelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: {
delegatorAddress,
validatorAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return false;
}
return msg;
return isTxMsgDelegate(msg);
};
const amountInAtomics = Decimal.fromUserInput(
amount || "0",
Number(state.chain.displayDenomExponent),
).atomics;
const msg: TxMsgDelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: {
delegatorAddress,
validatorAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
amount,
delegatorAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
state.chain.denom,
@@ -63,12 +78,12 @@ const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFor
validatorAddress,
]);
useEffect(() => {
setCheckAndGetMsg(() => checkAndGetMsg);
}, [checkAndGetMsg, setCheckAndGetMsg]);
return (
<>
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgDelegate</h2>
<div className="form-item">
<Input
label="Validator Address"
@@ -90,14 +105,22 @@ const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFor
/>
</div>
<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>
);
};
@@ -1,17 +1,25 @@
import { Decimal } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgRedelegate } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgRedelegate } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgRedelegateFormProps {
readonly delegatorAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegateFormProps) => {
const MsgRedelegateForm = ({
delegatorAddress,
setMsgGetter,
deleteMsg,
}: MsgRedelegateFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
@@ -23,50 +31,62 @@ const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegat
const [validatorDstAddressError, setValidatorDstAddressError] = useState("");
const [amountError, setAmountError] = useState("");
const checkAndGetMsg = useCallback(() => {
assert(state.chain.addressPrefix, "addressPrefix missing");
assert(state.chain.denom, "denom missing");
useEffect(() => {
try {
assert(state.chain.denom, "denom missing");
const srcAddressErrorMsg = checkAddress(validatorSrcAddress, state.chain.addressPrefix);
if (srcAddressErrorMsg) {
setValidatorSrcAddressError(
`Invalid address for network ${state.chain.chainId}: ${srcAddressErrorMsg}`,
);
return null;
}
setValidatorSrcAddressError("");
setValidatorDstAddressError("");
setAmountError("");
const dstAddressErrorMsg = checkAddress(validatorDstAddress, state.chain.addressPrefix);
if (dstAddressErrorMsg) {
setValidatorDstAddressError(
`Invalid address for network ${state.chain.chainId}: ${dstAddressErrorMsg}`,
);
return null;
}
const isMsgValid = (msg: TxMsg): msg is TxMsgRedelegate => {
assert(state.chain.addressPrefix, "addressPrefix missing");
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return null;
}
const srcAddressErrorMsg = checkAddress(validatorSrcAddress, state.chain.addressPrefix);
if (srcAddressErrorMsg) {
setValidatorSrcAddressError(
`Invalid address for network ${state.chain.chainId}: ${srcAddressErrorMsg}`,
);
return false;
}
const amountInAtomics = Decimal.fromUserInput(
amount,
Number(state.chain.displayDenomExponent),
).atomics;
const dstAddressErrorMsg = checkAddress(validatorDstAddress, state.chain.addressPrefix);
if (dstAddressErrorMsg) {
setValidatorDstAddressError(
`Invalid address for network ${state.chain.chainId}: ${dstAddressErrorMsg}`,
);
return false;
}
const msg: TxMsgRedelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate",
value: {
delegatorAddress,
validatorSrcAddress,
validatorDstAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return false;
}
return msg;
return isTxMsgRedelegate(msg);
};
const amountInAtomics = Decimal.fromUserInput(
amount || "0",
Number(state.chain.displayDenomExponent),
).atomics;
const msg: TxMsgRedelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate",
value: {
delegatorAddress,
validatorSrcAddress,
validatorDstAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
amount,
delegatorAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
state.chain.denom,
@@ -75,12 +95,12 @@ const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegat
validatorSrcAddress,
]);
useEffect(() => {
setCheckAndGetMsg(() => checkAndGetMsg);
}, [checkAndGetMsg, setCheckAndGetMsg]);
return (
<>
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgBeginRedelegate</h2>
<div className="form-item">
<Input
label="Source Validator Address"
@@ -112,14 +132,22 @@ const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegat
/>
</div>
<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>
);
};
@@ -1,17 +1,21 @@
import { Decimal } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgSend } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgSend } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgSendFormProps {
readonly fromAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => {
const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
@@ -21,39 +25,51 @@ const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => {
const [toAddressError, setToAddressError] = useState("");
const [amountError, setAmountError] = useState("");
const checkAndGetMsg = useCallback(() => {
assert(state.chain.addressPrefix, "addressPrefix missing");
assert(state.chain.denom, "denom missing");
useEffect(() => {
try {
assert(state.chain.denom, "denom missing");
const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setToAddressError(`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`);
return null;
}
setToAddressError("");
setAmountError("");
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return null;
}
const isMsgValid = (msg: TxMsg): msg is TxMsgSend => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const amountInAtomics = Decimal.fromUserInput(
amount,
Number(state.chain.displayDenomExponent),
).atomics;
const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setToAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return false;
}
const msg: TxMsgSend = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress,
toAddress,
amount: [{ amount: amountInAtomics, denom: state.chain.denom }],
},
};
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return false;
}
return msg;
return isTxMsgSend(msg);
};
const amountInAtomics = amount
? Decimal.fromUserInput(amount, Number(state.chain.displayDenomExponent)).atomics
: "0";
const msg: TxMsgSend = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress,
toAddress,
amount: [{ amount: amountInAtomics, denom: state.chain.denom }],
},
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
amount,
fromAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
state.chain.denom,
@@ -61,12 +77,12 @@ const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => {
toAddress,
]);
useEffect(() => {
setCheckAndGetMsg(() => checkAndGetMsg);
}, [checkAndGetMsg, setCheckAndGetMsg]);
return (
<>
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgSend</h2>
<div className="form-item">
<Input
label="Recipient Address"
@@ -88,14 +104,22 @@ const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => {
/>
</div>
<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>
);
};
@@ -0,0 +1,97 @@
import { assert } from "@cosmjs/utils";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgSetWithdrawAddress } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgSetWithdrawAddress } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgSetWithdrawAddressFormProps {
readonly delegatorAddress: string;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgSetWithdrawAddressForm = ({
delegatorAddress,
setMsgGetter,
deleteMsg,
}: MsgSetWithdrawAddressFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
const [withdrawAddress, setWithdrawAddress] = useState("");
const [withdrawAddressError, setWithdrawAddressError] = useState("");
useEffect(() => {
try {
setWithdrawAddressError("");
const isMsgValid = (msg: TxMsg): msg is TxMsgSetWithdrawAddress => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const addressErrorMsg = checkAddress(withdrawAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setWithdrawAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return false;
}
return isTxMsgSetWithdrawAddress(msg);
};
const msg: TxMsgSetWithdrawAddress = {
typeUrl: "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress",
value: { delegatorAddress, withdrawAddress },
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
delegatorAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
withdrawAddress,
]);
return (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgSetWithdrawAddress</h2>
<div className="form-item">
<Input
label="Withdraw Address"
name="withdraw-address"
value={withdrawAddress}
onChange={({ target }) => setWithdrawAddress(target.value)}
error={withdrawAddressError}
placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`}
/>
</div>
<style jsx>{`
.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 MsgSetWithdrawAddressForm;
@@ -1,17 +1,25 @@
import { Decimal } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
import { useAppContext } from "../../../../context/AppContext";
import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers";
import { isTxMsgUndelegate } from "../../../../lib/txMsgHelpers";
import { TxMsg, TxMsgUndelegate } from "../../../../types/txMsg";
import Input from "../../../inputs/Input";
import StackableContainer from "../../../layout/StackableContainer";
interface MsgUndelegateFormProps {
readonly delegatorAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegateFormProps) => {
const MsgUndelegateForm = ({
delegatorAddress,
setMsgGetter,
deleteMsg,
}: MsgUndelegateFormProps) => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
@@ -21,41 +29,52 @@ const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegat
const [validatorAddressError, setValidatorAddressError] = useState("");
const [amountError, setAmountError] = useState("");
const checkAndGetMsg = useCallback(() => {
assert(state.chain.addressPrefix, "addressPrefix missing");
assert(state.chain.denom, "denom missing");
useEffect(() => {
try {
assert(state.chain.denom, "denom missing");
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return null;
}
setValidatorAddressError("");
setAmountError("");
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return null;
}
const isMsgValid = (msg: TxMsg): msg is TxMsgUndelegate => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const amountInAtomics = Decimal.fromUserInput(
amount,
Number(state.chain.displayDenomExponent),
).atomics;
const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix);
if (addressErrorMsg) {
setValidatorAddressError(
`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
);
return false;
}
const msg: TxMsgUndelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate",
value: {
delegatorAddress,
validatorAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
if (!amount || Number(amount) <= 0) {
setAmountError("Amount must be greater than 0");
return false;
}
return msg;
return isTxMsgUndelegate(msg);
};
const amountInAtomics = Decimal.fromUserInput(
amount || "0",
Number(state.chain.displayDenomExponent),
).atomics;
const msg: TxMsgUndelegate = {
typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate",
value: {
delegatorAddress,
validatorAddress,
amount: { amount: amountInAtomics, denom: state.chain.denom },
},
};
setMsgGetter({ isMsgValid, msg });
} catch {}
}, [
amount,
delegatorAddress,
setMsgGetter,
state.chain.addressPrefix,
state.chain.chainId,
state.chain.denom,
@@ -63,12 +82,12 @@ const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegat
validatorAddress,
]);
useEffect(() => {
setCheckAndGetMsg(() => checkAndGetMsg);
}, [checkAndGetMsg, setCheckAndGetMsg]);
return (
<>
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgUndelegate</h2>
<div className="form-item">
<Input
label="Validator Address"
@@ -90,14 +109,22 @@ const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegat
/>
</div>
<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>
);
};
@@ -1,20 +1,21 @@
import { Dispatch, SetStateAction } from "react";
import { TxMsg, TxType } from "../../../../types/txMsg";
import { MsgGetter } from "..";
import { MsgType } from "../../../../types/txMsg";
import MsgClaimRewardsForm from "./MsgClaimRewardsForm";
import MsgDelegateForm from "./MsgDelegateForm";
import MsgRedelegateForm from "./MsgRedelegateForm";
import MsgSendForm from "./MsgSendForm";
import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm";
import MsgUndelegateForm from "./MsgUndelegateForm";
type MsgFormProps = {
readonly txType: TxType;
} & {
interface MsgFormProps {
readonly msgType: MsgType;
readonly senderAddress: string;
readonly setCheckAndGetMsg: Dispatch<SetStateAction<(() => TxMsg | null) | undefined>>;
};
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
readonly deleteMsg: () => void;
}
const MsgForm = ({ txType, senderAddress, ...restProps }: MsgFormProps) => {
switch (txType) {
const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => {
switch (msgType) {
case "send":
return <MsgSendForm fromAddress={senderAddress} {...restProps} />;
case "delegate":
@@ -25,6 +26,8 @@ const MsgForm = ({ txType, senderAddress, ...restProps }: MsgFormProps) => {
return <MsgRedelegateForm delegatorAddress={senderAddress} {...restProps} />;
case "claimRewards":
return <MsgClaimRewardsForm delegatorAddress={senderAddress} {...restProps} />;
case "setWithdrawAddress":
return <MsgSetWithdrawAddressForm delegatorAddress={senderAddress} {...restProps} />;
default:
return null;
}
+70 -43
View File
@@ -2,47 +2,59 @@ 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 { useRef, useState } from "react";
import { useAppContext } from "../../../context/AppContext";
import { capitalizeFirstLetter } from "../../../lib/displayHelpers";
import { TxMsg, TxType } from "../../../types/txMsg";
import { gasOfTx } from "../../../lib/txMsgHelpers";
import { MsgType, TxMsg } from "../../../types/txMsg";
import Button from "../../inputs/Button";
import Input from "../../inputs/Input";
import StackableContainer from "../../layout/StackableContainer";
import MsgForm from "./MsgForm";
interface CreateTxFormProps {
readonly router: NextRouter;
readonly txType: TxType;
readonly senderAddress: string;
readonly accountOnChain: Account;
readonly closeForm: () => void;
export interface MsgGetter {
readonly isMsgValid: (msg: TxMsg) => msg is TxMsg;
readonly msg: TxMsg;
}
const CreateTxForm = ({
router,
txType,
senderAddress,
accountOnChain,
closeForm,
}: CreateTxFormProps) => {
const getMsgFormKey = (msgType: MsgType, msg: TxMsg) => JSON.stringify({ msgType, msg });
interface CreateTxFormProps {
readonly router: NextRouter;
readonly senderAddress: string;
readonly accountOnChain: Account;
}
const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormProps) => {
const { state } = useAppContext();
const [processing, setProcessing] = useState(false);
const [checkAndGetMsg, setCheckAndGetMsg] = useState<() => TxMsg | null>();
const [msgTypes, setMsgTypes] = useState<readonly MsgType[]>([]);
const msgGetters = useRef<MsgGetter[]>([]);
const [memo, setMemo] = useState("");
const [gasLimit, setGasLimit] = useState(200000);
const [gasLimit, setGasLimit] = useState(gasOfTx([]));
const [gasLimitError, setGasLimitError] = useState("");
const gasPrice = state.chain.gasPrice;
assert(gasPrice, "gasPrice missing");
const addMsgType = (newMsgType: MsgType) => {
setMsgTypes((oldMsgTypes) => {
const newMsgTypes = [...oldMsgTypes, newMsgType];
setGasLimit(gasOfTx(newMsgTypes));
return newMsgTypes;
});
};
const createTx = async () => {
try {
assert(typeof accountOnChain.accountNumber === "number", "accountNumber missing");
assert(!!checkAndGetMsg, "form filled incorrectly");
const msg = checkAndGetMsg();
if (!msg) return;
assert(msgGetters.current.length, "form filled incorrectly");
const msgs = msgGetters.current
.filter(({ isMsgValid, msg }) => isMsgValid(msg))
.map(({ msg }) => msg);
if (!msgs.length || msgs.length !== msgTypes.length) return;
if (!Number.isSafeInteger(gasLimit) || gasLimit <= 0) {
setGasLimitError("gas limit must be a positive integer");
@@ -55,7 +67,7 @@ const CreateTxForm = ({
accountNumber: accountOnChain.accountNumber,
sequence: accountOnChain.sequence,
chainId: state.chain.chainId,
msgs: [msg],
msgs,
fee: calculateFee(gasLimit, gasPrice),
memo,
};
@@ -76,15 +88,30 @@ const CreateTxForm = ({
return (
<StackableContainer lessPadding>
<button className="remove" onClick={() => closeForm()}>
</button>
<h2>Create New {capitalizeFirstLetter(txType)} Transaction</h2>
<MsgForm
txType={txType}
senderAddress={senderAddress}
setCheckAndGetMsg={setCheckAndGetMsg}
/>
<h2>Create New Transaction</h2>
{msgTypes.map((msgType, index) => (
<MsgForm
key={getMsgFormKey(msgType, msgGetters.current[index]?.msg ?? {})}
msgType={msgType}
senderAddress={senderAddress}
setMsgGetter={(msgGetter) => {
msgGetters.current = [
...msgGetters.current.slice(0, index),
msgGetter,
...msgGetters.current.slice(index + 1),
];
}}
deleteMsg={() => {
msgGetters.current.splice(index, 1);
setMsgTypes((oldMsgTypes) => {
const newMsgTypes: MsgType[] = oldMsgTypes.slice();
newMsgTypes.splice(index, 1);
setGasLimit(gasOfTx(newMsgTypes));
return newMsgTypes;
});
}}
/>
))}
<div className="form-item">
<Input
type="number"
@@ -111,10 +138,21 @@ const CreateTxForm = ({
onChange={({ target }) => setMemo(target.value)}
/>
</div>
<StackableContainer>
<Button label="Add MsgSend" onClick={() => addMsgType("send")} />
<Button label="Add MsgDelegate" onClick={() => addMsgType("delegate")} />
<Button label="Add MsgUndelegate" onClick={() => addMsgType("undelegate")} />
<Button label="Add MsgBeginRedelegate" onClick={() => addMsgType("redelegate")} />
<Button label="Add MsgWithdrawDelegatorReward" onClick={() => addMsgType("claimRewards")} />
<Button
label="Add MsgSetWithdrawAddress"
onClick={() => addMsgType("setWithdrawAddress")}
/>
</StackableContainer>
<Button
label="Create Transaction"
onClick={createTx}
disabled={!checkAndGetMsg}
disabled={!msgTypes.length}
loading={processing}
/>
<style jsx>{`
@@ -124,17 +162,6 @@ const CreateTxForm = ({
.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>
);
+54 -9
View File
@@ -1,13 +1,16 @@
import { EncodeObject } from "@cosmjs/proto-signing";
import {
MsgType,
TxMsg,
TxMsgClaimRewards,
TxMsgDelegate,
TxMsgRedelegate,
TxMsgSend,
TxMsgSetWithdrawAddress,
TxMsgUndelegate,
} from "../types/txMsg";
const isTxMsgSend = (msg: EncodeObject): msg is TxMsgSend =>
const isTxMsgSend = (msg: TxMsg | EncodeObject): msg is TxMsgSend =>
msg.typeUrl === "/cosmos.bank.v1beta1.MsgSend" &&
"value" in msg &&
"fromAddress" in msg.value &&
@@ -17,7 +20,7 @@ const isTxMsgSend = (msg: EncodeObject): msg is TxMsgSend =>
!!msg.value.toAddress &&
!!msg.value.amount.length;
const isTxMsgDelegate = (msg: EncodeObject): msg is TxMsgDelegate =>
const isTxMsgDelegate = (msg: TxMsg | EncodeObject): msg is TxMsgDelegate =>
msg.typeUrl === "/cosmos.staking.v1beta1.MsgDelegate" &&
"value" in msg &&
"delegatorAddress" in msg.value &&
@@ -25,9 +28,9 @@ const isTxMsgDelegate = (msg: EncodeObject): msg is TxMsgDelegate =>
"amount" in msg.value &&
!!msg.value.delegatorAddress &&
!!msg.value.validatorAddress &&
!!msg.value.amount.length;
!!msg.value.amount;
const isTxMsgUndelegate = (msg: EncodeObject): msg is TxMsgUndelegate =>
const isTxMsgUndelegate = (msg: TxMsg | EncodeObject): msg is TxMsgUndelegate =>
msg.typeUrl === "/cosmos.staking.v1beta1.MsgUndelegate" &&
"value" in msg &&
"delegatorAddress" in msg.value &&
@@ -35,9 +38,9 @@ const isTxMsgUndelegate = (msg: EncodeObject): msg is TxMsgUndelegate =>
"amount" in msg.value &&
!!msg.value.delegatorAddress &&
!!msg.value.validatorAddress &&
!!msg.value.amount.length;
!!msg.value.amount;
const isTxMsgRedelegate = (msg: EncodeObject): msg is TxMsgRedelegate =>
const isTxMsgRedelegate = (msg: TxMsg | EncodeObject): msg is TxMsgRedelegate =>
msg.typeUrl === "/cosmos.staking.v1beta1.MsgBeginRedelegate" &&
"value" in msg &&
"delegatorAddress" in msg.value &&
@@ -47,9 +50,9 @@ const isTxMsgRedelegate = (msg: EncodeObject): msg is TxMsgRedelegate =>
!!msg.value.delegatorAddress &&
!!msg.value.validatorSrcAddress &&
!!msg.value.validatorDstAddress &&
!!msg.value.amount.length;
!!msg.value.amount;
const isTxMsgClaimRewards = (msg: EncodeObject): msg is TxMsgClaimRewards =>
const isTxMsgClaimRewards = (msg: TxMsg | EncodeObject): msg is TxMsgClaimRewards =>
msg.typeUrl === "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward" &&
"value" in msg &&
"delegatorAddress" in msg.value &&
@@ -57,4 +60,46 @@ const isTxMsgClaimRewards = (msg: EncodeObject): msg is TxMsgClaimRewards =>
!!msg.value.delegatorAddress &&
!!msg.value.validatorAddress;
export { isTxMsgSend, isTxMsgDelegate, isTxMsgUndelegate, isTxMsgRedelegate, isTxMsgClaimRewards };
const isTxMsgSetWithdrawAddress = (msg: TxMsg | EncodeObject): msg is TxMsgSetWithdrawAddress =>
msg.typeUrl === "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress" &&
"value" in msg &&
"delegatorAddress" in msg.value &&
"withdrawAddress" in msg.value &&
!!msg.value.delegatorAddress &&
!!msg.value.withdrawAddress;
const gasOfMsg = (msgType: MsgType): number => {
switch (msgType) {
case "send":
return 100_000;
case "delegate":
return 100_000;
case "undelegate":
return 100_000;
case "redelegate":
return 100_000;
case "claimRewards":
return 100_000;
case "setWithdrawAddress":
return 100_000;
default:
throw new Error("Unknown msg type");
}
};
const gasOfTx = (msgTypes: readonly MsgType[]): number => {
const txFlatGas = 100_000;
const totalTxGas = msgTypes.reduce((acc, msgType) => acc + gasOfMsg(msgType), txFlatGas);
return totalTxGas;
};
export {
isTxMsgSend,
isTxMsgDelegate,
isTxMsgUndelegate,
isTxMsgRedelegate,
isTxMsgClaimRewards,
isTxMsgSetWithdrawAddress,
gasOfMsg,
gasOfTx,
};
+36 -78
View File
@@ -14,7 +14,6 @@ import StackableContainer from "../../../components/layout/StackableContainer";
import { useAppContext } from "../../../context/AppContext";
import { explorerLinkAccount } from "../../../lib/displayHelpers";
import { getMultisigAccount } from "../../../lib/multisigHelpers";
import { TxType } from "../../../types/txMsg";
function participantPubkeysFromMultisig(
multisig: MultisigThresholdPubkey,
@@ -27,7 +26,6 @@ const Multipage = () => {
const { state } = useAppContext();
assert(state.chain.addressPrefix, "address prefix missing");
const [txType, setTxType] = useState<TxType | null>(null);
const [holdings, setHoldings] = useState<readonly Coin[]>([]);
const [multisigAddress, setMultisigAddress] = useState("");
const [accountOnChain, setAccountOnChain] = useState<Account | null>(null);
@@ -39,10 +37,6 @@ const Multipage = () => {
multisigAddress,
);
const closeForm = () => {
setTxType(null);
};
const fetchMultisig = useCallback(
async (address: string) => {
setAccountError(null);
@@ -92,83 +86,47 @@ const Multipage = () => {
threshold={pubkey.value.threshold}
/>
) : null}
{accountError ? (
<div className="interfaces">
<div className="col-1">
<MultisigHoldings holdings={holdings} />
</div>
<div className="col-2">
<StackableContainer lessPadding>
<h2>New transaction</h2>
<p>
Once a transaction is created, it can be signed by the multisig members, and then
broadcast.
</p>
</StackableContainer>
</div>
</div>
{accountError || !accountOnChain ? (
<StackableContainer>
<div className="multisig-error">
<p>
This multisig address's pubkeys are not available, and so it cannot be used with
this tool.
</p>
<p>
You can recreate it with this tool here, or sign and broadcast a transaction with
the tool you used to create it. Either option will make the pubkeys accessible and
will allow this tool to use this multisig fully.
</p>
{accountError ? (
<>
<p>
This multisig address's pubkeys are not available, and so it cannot be used with
this tool.
</p>
<p>
You can recreate it with this tool here, or sign and broadcast a transaction
with the tool you used to create it. Either option will make the pubkeys
accessible and will allow this tool to use this multisig fully.
</p>
</>
) : null}
{!!accountOnChain ? (
<p>
An account needs to be present on chain before creating a transaction. Send some
tokens to the address first.
</p>
) : null}
</div>
</StackableContainer>
) : null}
{!!txType && !!accountOnChain ? (
<CreateTxForm
txType={txType}
senderAddress={multisigAddress}
accountOnChain={accountOnChain}
closeForm={closeForm}
/>
) : null}
{txType === null ? (
<div className="interfaces">
<div className="col-1">
<MultisigHoldings holdings={holdings} />
</div>
<div className="col-2">
<StackableContainer lessPadding>
<h2>New transaction</h2>
<p>
Once a transaction is created, it can be signed by the multisig members, and then
broadcast.
</p>
{accountOnChain ? (
<>
<Button
label="Create Transaction"
onClick={() => {
setTxType("send");
}}
/>
<Button
label="Create Delegation"
onClick={() => {
setTxType("delegate");
}}
/>
<Button
label="Create Undelegation"
onClick={() => {
setTxType("undelegate");
}}
/>
<Button
label="Create Redelegate"
onClick={() => {
setTxType("redelegate");
}}
/>
<Button
label="Claim Rewards"
onClick={() => {
setTxType("claimRewards");
}}
/>
</>
) : (
<p>
An account needs to be present on chain before creating a transaction. Send some
tokens to the address first.
</p>
)}
</StackableContainer>
</div>
</div>
{!!accountOnChain ? (
<CreateTxForm senderAddress={multisigAddress} accountOnChain={accountOnChain} />
) : null}
</StackableContainer>
<style jsx>{`
+17 -2
View File
@@ -1,11 +1,18 @@
export type TxType = "send" | "delegate" | "undelegate" | "redelegate" | "claimRewards";
export type MsgType =
| "send"
| "delegate"
| "undelegate"
| "redelegate"
| "claimRewards"
| "setWithdrawAddress";
export type TxMsg =
| TxMsgSend
| TxMsgDelegate
| TxMsgUndelegate
| TxMsgRedelegate
| TxMsgClaimRewards;
| TxMsgClaimRewards
| TxMsgSetWithdrawAddress;
export interface TxMsgSend {
readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend";
@@ -51,3 +58,11 @@ export interface TxMsgClaimRewards {
readonly validatorAddress: string;
};
}
export interface TxMsgSetWithdrawAddress {
readonly typeUrl: "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress";
readonly value: {
readonly delegatorAddress: string;
readonly withdrawAddress: string;
};
}