Merge pull request #73 from klk1236/klk/delegate-support

Klk/delegate support
This commit is contained in:
samepant 2022-10-22 16:35:59 -04:00 committed by GitHub
commit 9ccb80aca3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 281 additions and 38 deletions

5
.gitignore vendored
View File

@ -21,4 +21,7 @@ yarn-debug.log*
yarn-error.log*
# Local Netlify folder
.netlify
.netlify
# IDE folder
.idea

View File

@ -5,7 +5,7 @@ import { DbTransaction } from "../../types";
import { useAppContext } from "../../context/AppContext";
import HashView from "./HashView";
import StackableContainer from "../layout/StackableContainer";
import { printableCoins } from "../../lib/displayHelpers";
import { printableCoins, printableCoin } from "../../lib/displayHelpers";
interface Props {
tx: DbTransaction;
@ -16,38 +16,75 @@ const TransactionInfo = (props: Props) => {
return (
<StackableContainer lessPadding lessMargin>
<ul className="meta-data">
{props.tx.msgs && (
<li>
<label>Amount:</label>
<div>{printableCoins(props.tx.msgs[0].value.amount, state.chain)}</div>
</li>
)}
{props.tx.msgs && (
<li>
<label>To:</label>
<div title={props.tx.msgs[0].value.toAddress}>
<HashView hash={props.tx.msgs[0].value.toAddress} />
</div>
</li>
)}
{props.tx.fee && (
<>
<>
{(props.tx.msgs || []).map((msg) =>
msg.typeUrl === "/cosmos.bank.v1beta1.MsgSend" ? (
<>
<li>
<label>Amount:</label>
<div>{printableCoins(msg.value.amount, state.chain)}</div>
</li>
<li>
<label>To:</label>
<div title={msg.value.toAddress}>
<HashView hash={msg.value.toAddress} />
</div>
</li>
</>
) : msg.typeUrl === "/cosmos.staking.v1beta1.MsgDelegate" ||
msg.typeUrl === "/cosmos.staking.v1beta1.MsgUnDelegate" ? (
<>
<li>
<label>Amount:</label>
<div>{printableCoin(props.tx.msgs[0].value.amount, state.chain)}</div>
</li>
<li>
<label>Validator Address:</label>
<div title={props.tx.msgs[0].value.validatorAddress}>
<HashView hash={props.tx.msgs[0].value.validatorAddress} />
</div>
</li>
</>
) : msg.typeUrl === "/cosmos.staking.v1beta1.MsgBeginRedelegate" ? (
<>
<li>
<label>Amount:</label>
<div>{printableCoin(props.tx.msgs[0].value.amount, state.chain)}</div>
</li>
<li>
<label>Source Validator Address:</label>
<div title={props.tx.msgs[0].value.validatorSrcAddress}>
<HashView hash={props.tx.msgs[0].value.validatorSrcAddress} />
</div>
</li>
<li>
<label>Destination Validator Address:</label>
<div title={props.tx.msgs[0].value.validatorDstAddress}>
<HashView hash={props.tx.msgs[0].value.validatorDstAddress} />
</div>
</li>
</>
) : null,
)}
{props.tx.fee && (
<>
<li>
<label>Gas:</label>
<div>{props.tx.fee.gas}</div>
</li>
<li>
<label>Fee:</label>
<div>{printableCoins(props.tx.fee.amount as Coin[], state.chain)}</div>
</li>
</>
)}
{props.tx.memo && (
<li>
<label>Gas:</label>
<div>{props.tx.fee.gas}</div>
<label>Memo:</label>
<div>{props.tx.memo}</div>
</li>
<li>
<label>Fee:</label>
<div>{printableCoins(props.tx.fee.amount as Coin[], state.chain)}</div>
</li>
</>
)}
{props.tx.memo && (
<li>
<label>Memo:</label>
<div>{props.tx.memo}</div>
</li>
)}
)}
</>
</ul>
<style jsx>{`
ul {

View File

@ -0,0 +1,155 @@
import axios from "axios";
import { Account, calculateFee } from "@cosmjs/stargate";
import { Decimal } from "@cosmjs/math";
import { assert } from "@cosmjs/utils";
import React, { useState } from "react";
import { withRouter, NextRouter } from "next/router";
import { useAppContext } from "../../context/AppContext";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";
import { checkValidatorAddress } from "../../lib/displayHelpers";
interface Props {
address: string | null;
accountOnChain: Account | null;
router: NextRouter;
closeForm: () => void;
}
const DelegationForm = (props: Props) => {
const { state } = useAppContext();
const [validatorAddress, setValidatorAddress] = useState("");
const [amount, setAmount] = useState("0");
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, txAmount: string, txGas: number) => {
const amountInAtomics = Decimal.fromUserInput(
txAmount,
Number(state.chain.displayDenomExponent),
).atomics;
const msgDelegate = {
delegatorAddress: props.address,
validatorAddress: txValidatorAddress,
amount: {
amount: amountInAtomics,
denom: state.chain.denom,
},
};
const msg = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: msgDelegate,
};
assert(gasPrice, "gasPrice missing");
const fee = calculateFee(Number(txGas), 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 = checkValidatorAddress(validatorAddress, "cosmosvaloper");
if (validatorAddressError) {
setAddressError(
`Invalid address for network ${state.chain.chainId}: ${validatorAddressError}`,
);
return;
}
setProcessing(true);
const tx = createTransaction(validatorAddress, 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 Delegation</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. cosmosvaloper1sjllsnramtg3ewxqwwrwjxfgc4n4ef9u2lcnj0`}
/>
</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="Delegate" 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(DelegationForm);

View File

@ -126,6 +126,32 @@ const checkAddress = (input: string, chainAddressPrefix: string) => {
return null;
};
/**
* Returns an error message for invalid addresses.
*
* Returns null of there is no error.
*/
const checkValidatorAddress = (input: string, chainAddressPrefix: string): string | null => {
if (!input) return "Empty";
let prefix;
try {
({ prefix } = fromBech32(input));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return error.toString();
}
if (prefix !== chainAddressPrefix) {
return `Expected address prefix '${chainAddressPrefix}' but got '${prefix}'`;
}
if (input.length !== 52) {
return "Invalid address length in validator address. Must be 52 bytes.";
}
return null;
};
/**
* Returns a link to a transaction in an explorer if an explorer is configured
* for transactions. Returns null otherwise.
@ -145,4 +171,5 @@ export {
examplePubkey,
checkAddress,
explorerLinkTx,
checkValidatorAddress,
};

View File

@ -14,6 +14,7 @@ import MultisigMembers from "../../../components/dataViews/MultisigMembers";
import Page from "../../../components/layout/Page";
import StackableContainer from "../../../components/layout/StackableContainer";
import TransactionForm from "../../../components/forms/TransactionForm";
import DelegationForm from "../../../components/forms/DelegationForm";
function participantPubkeysFromMultisig(multisigPubkey: Pubkey) {
return multisigPubkey.value.pubkeys;
@ -27,7 +28,8 @@ function participantAddressesFromMultisig(multisigPubkey: Pubkey, addressPrefix:
const multipage = () => {
const { state } = useAppContext();
const [showTxForm, setShowTxForm] = useState(false);
const [showSendTxForm, setShowSendTxForm] = useState(false);
const [showDelegateTxForm, setShowDelegateTxForm] = useState(false);
const [holdings, setHoldings] = useState<Coin | null>(null);
const [multisigAddress, setMultisigAddress] = useState("");
const [accountOnChain, setAccountOnChain] = useState<Account | null>(null);
@ -97,15 +99,25 @@ const multipage = () => {
</div>
</StackableContainer>
)}
{showTxForm ? (
{showSendTxForm && (
<TransactionForm
address={multisigAddress}
accountOnChain={accountOnChain}
closeForm={() => {
setShowTxForm(false);
setShowSendTxForm(false);
}}
/>
) : (
)}
{showDelegateTxForm && (
<DelegationForm
address={multisigAddress}
accountOnChain={accountOnChain}
closeForm={() => {
setShowDelegateTxForm(false);
}}
/>
)}
{!showSendTxForm && !showDelegateTxForm && (
<div className="interfaces">
<div className="col-1">
<MultisigHoldings holdings={holdings} />
@ -120,7 +132,13 @@ const multipage = () => {
<Button
label="Create Transaction"
onClick={() => {
setShowTxForm(true);
setShowSendTxForm(true);
}}
/>
<Button
label="Create Delegation"
onClick={() => {
setShowDelegateTxForm(true);
}}
/>
</StackableContainer>
@ -133,10 +151,12 @@ const multipage = () => {
display: flex;
justify-content: space-between;
margin-top: 50px;
flex-direction: column;
}
.col-1 {
flex: 1;
padding-right: 50px;
padding-right: 0;
margin-bottom: 50px;
}
.col-2 {
flex: 1;
@ -147,6 +167,7 @@ const multipage = () => {
}
p {
margin-top: 15px;
max-width: 100%;
}
.multisig-error p {
max-width: 550px;