Merge pull request #61 from cosmos/samepant/typescript

Typescript conversion and Nextjs version upgrade
This commit is contained in:
samepant
2022-06-09 18:48:55 -04:00
committed by GitHub
43 changed files with 1139 additions and 3808 deletions
@@ -1,6 +1,8 @@
import React from "react";
const DevHelper = (_props) => (
interface Props {}
const DevHelper = (_props: Props) => (
<div className="dev-helper">
<h3>Dev Helper</h3>
<h4>Pages</h4>
@@ -4,18 +4,40 @@ import { StargateClient } from "@cosmjs/stargate";
import GearIcon from "../icons/Gear";
import Button from "../inputs/Button";
import Input from "../../components/inputs/Input";
import Input from "../inputs/Input";
import { useAppContext } from "../../context/AppContext";
import Select from "../inputs/Select";
import StackableContainer from "../layout/StackableContainer";
interface ChainOption {
label: string;
value: number;
}
interface GithubChainRegistryItem {
name: string;
path: string;
sha: string;
size: number;
url: string;
html_url: string;
git_url: string;
download_url: string | null;
type: string;
_links: {
self: string;
git: string;
html: string;
};
}
const ChainSelect = () => {
const { state, dispatch } = useAppContext();
// UI State
const [chainArray, setChainArray] = useState();
const [chainOptions, setChainOptions] = useState();
const [chainError, setChainError] = useState();
const [chainArray, setChainArray] = useState([]);
const [chainOptions, setChainOptions] = useState<ChainOption[]>([]);
const [chainError, setChainError] = useState<string | null>(null);
const [showSettings, setShowSettings] = useState(false);
const [selectValue, setSelectValue] = useState({ label: "Loading...", value: -1 });
@@ -57,23 +79,23 @@ const ChainSelect = () => {
// getting chain info from this repo: https://github.com/cosmos/chain-registry
try {
const res = await axios.get(url);
const chains = res.data.filter((item) => {
const chains = res.data.filter((item: GithubChainRegistryItem) => {
return item.type == "dir" && !item.name.startsWith(".") && item.name != "testnets";
});
setChainArray(chains);
const options = chains.map(({ name }, index) => {
const options = chains.map(({ name }: GithubChainRegistryItem, index: number) => {
return { label: name, value: index };
});
setChainOptions(options);
setSelectValue(findExistingOption(options, state.chain.registryName));
} catch (error) {
setSelectValue(findExistingOption(options, state!.chain.registryName!));
} catch (error: any) {
console.log(error);
setShowSettings(true);
setChainError(error.message);
}
};
const findExistingOption = (options, registryName) => {
const findExistingOption = (options: ChainOption[], registryName: string) => {
const index = options.findIndex((option) => option.label === registryName);
if (index >= 0) {
return options[index];
@@ -81,7 +103,7 @@ const ChainSelect = () => {
return { label: "unkown chain", value: -1 };
};
const getChainInfo = async (chainOption) => {
const getChainInfo = async (chainOption: GithubChainRegistryItem) => {
setChainError(null);
try {
const chainInfoUrl =
@@ -102,7 +124,7 @@ const ChainSelect = () => {
const chainDisplayName = chainData["pretty_name"];
const registryName = chainOption.name;
const explorerLink = getExplorerFromArray(chainData.explorers);
let asset = "";
let asset = null;
let denom = "";
let displayDenom = "";
const displayDenomExponent = 6;
@@ -143,21 +165,21 @@ const ChainSelect = () => {
},
});
setShowSettings(false);
} catch (error) {
} catch (error: any) {
console.log(error);
setShowSettings(true);
setChainError(error.message);
}
};
const getExplorerFromArray = (array) => {
const getExplorerFromArray = (array: { kind: string; url: string; tx_page: string }[]) => {
if (array && array.length > 0) {
return array[0]["tx_page"];
}
return "";
};
const getNodeFromArray = (nodeArray) => {
const getNodeFromArray = (nodeArray: { address: string; provider: string }[]) => {
// only return https connections
const secureNodes = nodeArray.filter((node) => node.address.includes("https://"));
if (secureNodes.length === 0) {
@@ -166,7 +188,7 @@ const ChainSelect = () => {
return secureNodes[0].address;
};
const onChainSelect = (option) => {
const onChainSelect = (option: ChainOption) => {
const index = chainOptions.findIndex((opt) => opt.label === option.label);
setSelectValue(chainOptions[index]);
getChainInfo(chainArray[option.value]);
@@ -176,7 +198,7 @@ const ChainSelect = () => {
setChainError(null);
try {
// test client connection
const client = await StargateClient.connect(tempNodeAddress);
const client = await StargateClient.connect(tempNodeAddress!);
await client.getHeight();
// change app state
@@ -195,10 +217,10 @@ const ChainSelect = () => {
explorerLink: tempExplorerLink,
},
});
const selectedOption = findExistingOption(chainOptions, tempRegistryName);
const selectedOption = findExistingOption(chainOptions, tempRegistryName!);
setSelectValue(selectedOption);
setShowSettings(false);
} catch (error) {
} catch (error: any) {
console.log(error);
setShowSettings(true);
setChainError(error.message);
@@ -237,14 +259,16 @@ const ChainSelect = () => {
<Input
width="48%"
value={tempChainName}
onChange={(e) => setChainName(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setChainName(e.target.value)
}
label="Chain Name"
/>
<Input
width="48%"
value={tempChainId}
onChange={(e) => setChainId(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setChainId(e.target.value)}
label="Chain ID"
/>
</div>
@@ -252,13 +276,17 @@ const ChainSelect = () => {
<Input
width="48%"
value={tempAddressPrefix}
onChange={(e) => setAddressPrefix(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAddressPrefix(e.target.value)
}
label="Bech32 Prefix (address prefix)"
/>
<Input
width="48%"
value={tempNodeAddress}
onChange={(e) => setNodeAddress(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNodeAddress(e.target.value)
}
label="RPC Node URL (must be https)"
/>
</div>
@@ -266,13 +294,15 @@ const ChainSelect = () => {
<Input
width="48%"
value={tempDisplayDenom}
onChange={(e) => setDisplayDenom(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setDisplayDenom(e.target.value)
}
label="Display Denom"
/>
<Input
width="48%"
value={tempDenom}
onChange={(e) => setDenom(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDenom(e.target.value)}
label="Base Denom"
/>
</div>
@@ -280,13 +310,15 @@ const ChainSelect = () => {
<Input
width="48%"
value={tempDisplayDenomExponent}
onChange={(e) => setDisplayDenomExponent(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setDisplayDenomExponent(parseInt(e.target.value))
}
label="Denom Exponent"
/>
<Input
width="48%"
value={tempGasPrice}
onChange={(e) => setGasPrice(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGasPrice(e.target.value)}
label="Gas Price"
/>
</div>
@@ -294,7 +326,9 @@ const ChainSelect = () => {
<Input
width="48%"
value={tempExplorerLink}
onChange={(e) => setExplorerLink(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setExplorerLink(e.target.value)
}
label="Explorer Link (with '${txHash}' included)"
/>
</div>
@@ -6,9 +6,14 @@ import Button from "../inputs/Button";
import { explorerLinkTx } from "../../lib/displayHelpers";
import { useAppContext } from "../../context/AppContext";
const CompletedTransaction = ({ transactionHash }) => {
interface Props {
transactionHash: string;
}
const CompletedTransaction = ({ transactionHash }: Props) => {
const { state } = useAppContext();
const explorerLink = explorerLinkTx(state.chain.explorerLink, transactionHash);
const baseURL = state.chain.explorerLink ? state.chain.explorerLink : "";
const explorerLink = explorerLinkTx(baseURL, transactionHash);
return (
<StackableContainer lessPadding lessMargin>
<StackableContainer lessPadding lessMargin lessRadius>
@@ -1,7 +1,13 @@
import React from "react";
import copy from "copy-to-clipboard";
const CopyAndPaste = (props) => (
interface Props {
copyText: string;
stroke?: string;
strokeWidth?: number;
}
const CopyAndPaste = (props: Props) => (
<div className="icon" onClick={() => copy(props.copyText)}>
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 32H41C53.1503 32 63 41.8497 63 54V94H13V32Z" />
@@ -3,10 +3,15 @@ import React from "react";
import { abbreviateLongString } from "../../lib/displayHelpers";
import CopyAndPaste from "./CopyAndPaste";
const HashView = ({ hash, abbreviate }) => (
interface Props {
hash: string;
abbreviate?: boolean;
}
const HashView = (props: Props) => (
<div className="hash-view">
<div>{abbreviate ? abbreviateLongString(hash) : hash}</div>
<CopyAndPaste copyText={hash} />
<div>{props.abbreviate ? abbreviateLongString(props.hash) : props.hash}</div>
<CopyAndPaste copyText={props.hash} />
<style jsx>{`
.hash-view {
display: flex;
@@ -1,16 +1,25 @@
import React from "react";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { useAppContext } from "../../context/AppContext";
import { printableCoin } from "../../lib/displayHelpers";
import StackableContainer from "../layout/StackableContainer";
const MultisigHoldings = (props) => {
interface Props {
holdings: Coin | null;
}
const MultisigHoldings = (props: Props) => {
const { state } = useAppContext();
return (
<StackableContainer lessPadding fullHeight>
<h2>Holdings</h2>
<StackableContainer lessPadding lessMargin>
<span>{printableCoin(props.holdings, state.chain)}</span>
{props.holdings ? (
<span>{printableCoin(props.holdings, state.chain)}</span>
) : (
<span>None</span>
)}
</StackableContainer>
<style jsx>{`
span {
@@ -3,7 +3,13 @@ import React from "react";
import HashView from "./HashView";
import StackableContainer from "../layout/StackableContainer";
const MultisigMembers = (props) => (
interface Props {
/** Addresses of the multisig members */
members: string[];
threshold: number;
}
const MultisigMembers = (props: Props) => (
<StackableContainer lessPadding>
<div className="meta-data">
<div>
@@ -13,7 +19,7 @@ const MultisigMembers = (props) => (
<div>
<h2>Members</h2>
<ul>
{props.members.map((address) => (
{props.members.map((address: string) => (
<li key={address} className="info">
<HashView hash={address} />
</li>
@@ -1,8 +1,14 @@
import React from "react";
import { Account } from "@cosmjs/stargate";
import { DbSignature } from "../../types";
import StackableContainer from "../layout/StackableContainer";
const ThresholdInfo = ({ signatures, account }) => (
interface Props {
signatures: DbSignature[];
account: Account;
}
const ThresholdInfo = ({ signatures, account }: Props) => (
<StackableContainer lessPadding lessMargin>
<h2>Signatures</h2>
<StackableContainer lessPadding lessMargin lessRadius>
@@ -15,7 +21,7 @@ const ThresholdInfo = ({ signatures, account }) => (
<div className="threshold">
<div className="current">{signatures.length}</div>
<div className="label divider">of</div>
<div className="required">{account.pubkey.value.threshold}</div>
<div className="required">{account.pubkey!.value.threshold}</div>
<div className="label">signatures complete</div>
</div>
</StackableContainer>
@@ -1,11 +1,17 @@
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 StackableContainer from "../layout/StackableContainer";
import { printableCoins } from "../../lib/displayHelpers";
const TransactionInfo = (props) => {
interface Props {
tx: DbTransaction;
}
const TransactionInfo = (props: Props) => {
const { state } = useAppContext();
return (
<StackableContainer lessPadding lessMargin>
@@ -32,7 +38,7 @@ const TransactionInfo = (props) => {
</li>
<li>
<label>Fee:</label>
<div>{printableCoins(props.tx.fee.amount, state.chain)}</div>
<div>{printableCoins(props.tx.fee.amount as Coin[], state.chain)}</div>
</li>
</>
)}
-27
View File
@@ -1,27 +0,0 @@
import React from "react";
import { withRouter } from "next/router";
import Button from "../../components/inputs/Button";
import StackableContainer from "../layout/StackableContainer";
const TransactionForm = () => {
return (
<StackableContainer lessPadding>
<h2>New transaction</h2>
<p>Connect your wallet to create a new transaction.</p>
<Button
label="Connect Wallet"
onClick={() => {
this.props.onConnect(true);
}}
/>
<style jsx>{`
p {
margin-top: 15px;
}
`}</style>
</StackableContainer>
);
};
export default withRouter(TransactionForm);
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { withRouter } from "next/router";
import { withRouter, NextRouter } from "next/router";
import { useAppContext } from "../../context/AppContext";
import Button from "../inputs/Button";
@@ -7,7 +7,11 @@ import StackableContainer from "../layout/StackableContainer";
import Input from "../inputs/Input";
import { exampleAddress } from "../../lib/displayHelpers";
const FindMultisigForm = (props) => {
interface Props {
router: NextRouter;
}
const FindMultisigForm = (props: Props) => {
const { state } = useAppContext();
const [address, setAddress] = useState("");
const [_processing, setProcessing] = useState(false);
@@ -28,11 +32,11 @@ const FindMultisigForm = (props) => {
</StackableContainer>
<StackableContainer lessPadding lessMargin>
<Input
onChange={(e) => setAddress(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAddress(e.target.value)}
value={address}
label="Multisig Address"
name="address"
placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`}
placeholder={`E.g. ${exampleAddress(0, state!.chain.addressPrefix!)}`}
/>
<Button label="Use this Multisig" onClick={handleSearch} primary />
</StackableContainer>
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { withRouter } from "next/router";
import { withRouter, NextRouter } from "next/router";
import { StargateClient } from "@cosmjs/stargate";
import { useAppContext } from "../../context/AppContext";
@@ -14,23 +14,31 @@ const emptyPubKeyGroup = () => {
return { address: "", compressedPubkey: "", keyError: "", isPubkey: false };
};
const MultiSigForm = (props) => {
interface Props {
router: NextRouter;
}
const MultiSigForm = (props: Props) => {
const { state } = useAppContext();
const [pubkeys, setPubkeys] = useState([emptyPubKeyGroup(), emptyPubKeyGroup()]);
const [threshold, setThreshold] = useState(2);
const [_processing, setProcessing] = useState(false);
const handleChangeThreshold = (e) => {
let newThreshold = e.target.value;
const handleChangeThreshold = (e: React.ChangeEvent<HTMLInputElement>) => {
let newThreshold = parseInt(e.target.value);
if (newThreshold > pubkeys.length || newThreshold <= 0) {
newThreshold = threshold;
}
setThreshold(newThreshold);
};
const handleKeyGroupChange = (index, e) => {
const handleKeyGroupChange = (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
const tempPubkeys = [...pubkeys];
tempPubkeys[index][e.target.name] = e.target.value;
if (e.target.name === "compressedPubkey") {
tempPubkeys[index].compressedPubkey = e.target.value;
} else if (e.target.name === "address") {
tempPubkeys[index].address = e.target.value;
}
setPubkeys(tempPubkeys);
};
@@ -39,7 +47,7 @@ const MultiSigForm = (props) => {
setPubkeys(tempPubkeys.concat(emptyPubKeyGroup()));
};
const handleRemove = (index) => {
const handleRemove = (index: number) => {
const tempPubkeys = [...pubkeys];
const oldLength = tempPubkeys.length;
tempPubkeys.splice(index, 1);
@@ -48,8 +56,8 @@ const MultiSigForm = (props) => {
setThreshold(newThreshold);
};
const getPubkeyFromNode = async (address) => {
const client = await StargateClient.connect(state.chain.nodeAddress);
const getPubkeyFromNode = async (address: string) => {
const client = await StargateClient.connect(state!.chain.nodeAddress!);
const accountOnChain = await client.getAccount(address);
console.log(accountOnChain);
if (!accountOnChain || !accountOnChain.pubkey) {
@@ -60,7 +68,7 @@ const MultiSigForm = (props) => {
return accountOnChain.pubkey.value;
};
const handleKeyBlur = async (index, e) => {
const handleKeyBlur = async (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
try {
const tempPubkeys = [...pubkeys];
let pubkey;
@@ -82,7 +90,7 @@ const MultiSigForm = (props) => {
tempPubkeys[index].compressedPubkey = pubkey;
tempPubkeys[index].keyError = "";
setPubkeys(tempPubkeys);
} catch (error) {
} catch (error: any) {
console.log(error);
const tempPubkeys = [...pubkeys];
tempPubkeys[index].keyError = error.message;
@@ -97,9 +105,9 @@ const MultiSigForm = (props) => {
try {
multisigAddress = await createMultisigFromCompressedSecp256k1Pubkeys(
compressedPubkeys,
parseInt(threshold, 10),
state.chain.addressPrefix,
state.chain.chainId,
threshold,
state!.chain.addressPrefix!,
state!.chain.chainId!,
);
props.router.push(`/multi/${multisigAddress}`);
} catch (error) {
@@ -107,7 +115,7 @@ const MultiSigForm = (props) => {
}
};
const togglePubkey = (index) => {
const togglePubkey = (index: number) => {
const tempPubkeys = [...pubkeys];
tempPubkeys[index].isPubkey = !tempPubkeys[index].isPubkey;
setPubkeys(tempPubkeys);
@@ -134,7 +142,7 @@ const MultiSigForm = (props) => {
)}
<div className="key-inputs">
<Input
onChange={(e) => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
handleKeyGroupChange(index, e);
}}
value={pubkeyGroup.isPubkey ? pubkeyGroup.compressedPubkey : pubkeyGroup.address}
@@ -144,10 +152,10 @@ const MultiSigForm = (props) => {
placeholder={`E.g. ${
pubkeyGroup.isPubkey
? examplePubkey(index)
: exampleAddress(index, state.chain.addressPrefix)
: exampleAddress(index, state!.chain.addressPrefix!)
}`}
error={pubkeyGroup.keyError}
onBlur={(e) => {
onBlur={(e: React.ChangeEvent<HTMLInputElement>) => {
handleKeyBlur(index, e);
}}
/>
@@ -1,29 +1,36 @@
import axios from "axios";
import { calculateFee } from "@cosmjs/stargate";
import { calculateFee, Account } from "@cosmjs/stargate";
import { Decimal } from "@cosmjs/math";
import React, { useState } from "react";
import { withRouter } from "next/router";
import { withRouter, NextRouter } from "next/router";
import { useAppContext } from "../../context/AppContext";
import Button from "../../components/inputs/Button";
import Input from "../../components/inputs/Input";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";
import { checkAddress, exampleAddress } from "../../lib/displayHelpers";
const TransactionForm = (props) => {
interface Props {
address: string | null;
accountOnChain: Account | null;
router: NextRouter;
closeForm: () => void;
}
const TransactionForm = (props: Props) => {
const { state } = useAppContext();
const [toAddress, setToAddress] = useState("");
const [amount, setAmount] = useState("0");
const [memo, setMemo] = useState("");
const [gas, setGas] = useState(200000);
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
const [gasPrice, _setGasPrice] = useState(state!.chain.gasPrice);
const [_processing, setProcessing] = useState(false);
const [addressError, setAddressError] = useState("");
const createTransaction = (txToAddress, txAmount, txGas) => {
const createTransaction = (txToAddress: string, txAmount: string, txGas: number) => {
const amountInAtomics = Decimal.fromUserInput(
txAmount,
Number(state.chain.displayDenomExponent),
Number(state!.chain.displayDenomExponent),
).atomics;
const msgSend = {
fromAddress: props.address,
@@ -31,7 +38,7 @@ const TransactionForm = (props) => {
amount: [
{
amount: amountInAtomics,
denom: state.chain.denom,
denom: state!.chain.denom,
},
],
};
@@ -39,11 +46,11 @@ const TransactionForm = (props) => {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: msgSend,
};
const fee = calculateFee(Number(txGas), gasPrice);
const fee = calculateFee(Number(txGas), gasPrice!);
return {
accountNumber: props.accountOnChain.accountNumber,
sequence: props.accountOnChain.sequence,
chainId: state.chain.chainId,
accountNumber: props.accountOnChain!.accountNumber,
sequence: props.accountOnChain!.sequence,
chainId: state!.chain.chainId,
msgs: [msg],
fee: fee,
memo: memo,
@@ -51,9 +58,9 @@ const TransactionForm = (props) => {
};
const handleCreate = async () => {
const toAddressError = checkAddress(toAddress, state.chain.addressPrefix);
const toAddressError = checkAddress(toAddress, state!.chain.addressPrefix!);
if (toAddressError) {
setAddressError(`Invalid address for network ${state.chain.chainId}: ${toAddressError}`);
setAddressError(`Invalid address for network ${state!.chain.chainId}: ${toAddressError}`);
return;
}
@@ -77,18 +84,18 @@ const TransactionForm = (props) => {
label="To Address"
name="toAddress"
value={toAddress}
onChange={(e) => setToAddress(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setToAddress(e.target.value)}
error={addressError}
placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`}
placeholder={`E.g. ${exampleAddress(0, state!.chain.addressPrefix!)}`}
/>
</div>
<div className="form-item">
<Input
label={`Amount (${state.chain.displayDenom})`}
label={`Amount (${state!.chain.displayDenom})`}
name="amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAmount(e.target.value)}
/>
</div>
<div className="form-item">
@@ -97,7 +104,7 @@ const TransactionForm = (props) => {
name="gas"
type="number"
value={gas}
onChange={(e) => setGas(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGas(parseInt(e.target.value))}
/>
</div>
<div className="form-item">
@@ -109,7 +116,7 @@ const TransactionForm = (props) => {
name="memo"
type="text"
value={memo}
onChange={(e) => setMemo(e.target.value)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
/>
</div>
<Button label="Create Transaction" onClick={handleCreate} />
@@ -8,23 +8,32 @@ import { useAppContext } from "../../context/AppContext";
import Button from "../inputs/Button";
import HashView from "../dataViews/HashView";
import StackableContainer from "../layout/StackableContainer";
import { DbSignature, DbTransaction, WalletAccount } from "../../types";
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
import { LedgerSigner } from "@cosmjs/ledger-amino";
import { makeCosmoshubPath } from "@cosmjs/amino";
const TransactionSigning = (props) => {
interface Props {
signatures: DbSignature[];
tx: DbTransaction;
transactionID: string;
addSignature: (signature: DbSignature) => void;
}
const TransactionSigning = (props: Props) => {
const { state } = useAppContext();
const [walletAccount, setWalletAccount] = useState(null);
const [sigError, setSigError] = useState(null);
const [walletAccount, setWalletAccount] = useState<WalletAccount>();
const [sigError, setSigError] = useState("");
const [hasSigned, setHasSigned] = useState(false);
const [walletType, setWalletType] = useState("");
const [ledgerSigner, setLedgerSigner] = useState({});
const connectKeplr = async () => {
try {
await window.keplr.enable(state.chain.chainId);
const tempWalletAccount = await window.keplr.getKey(state.chain.chainId);
await window.keplr.enable(state!.chain.chainId!);
const tempWalletAccount = await window.keplr.getKey(state!.chain.chainId!);
console.log(tempWalletAccount);
const tempHasSigned = props.signatures.some(
(sig) => sig.address === tempWalletAccount.bech32Address,
);
@@ -43,11 +52,16 @@ const TransactionSigning = (props) => {
// Setup signer
const offlineSigner = new LedgerSigner(ledgerTransport, {
hdPaths: [makeCosmoshubPath(0)],
prefix: state.chain.addressPrefix,
prefix: state!.chain.addressPrefix,
});
console.log(offlineSigner);
const accounts = await offlineSigner.getAccounts();
const tempWalletAccount = accounts[0].address;
console.log(accounts);
const tempWalletAccount: WalletAccount = {
bech32Address: accounts[0].address,
pubkey: accounts[0].pubkey,
algo: accounts[0].algo,
};
const tempHasSigned = props.signatures.some(
(sig) => sig.address === tempWalletAccount.bech32Address,
@@ -60,19 +74,18 @@ const TransactionSigning = (props) => {
const signTransaction = async () => {
const offlineSigner =
walletType === "keplr" ? window.getOfflineSignerOnlyAmino(state.chain.chainId) : ledgerSigner;
const signerAddress =
walletType === "keplr"
? walletAccount.bech32Address
: (await ledgerSigner.getAccounts())[0]?.address;
? window.getOfflineSignerOnlyAmino(state!.chain.chainId)
: ledgerSigner;
const signerAddress = walletAccount?.bech32Address;
assert(signerAddress, "Missing signer address");
const signingClient = await SigningStargateClient.offline(offlineSigner);
const signerData = {
accountNumber: props.tx.accountNumber,
sequence: props.tx.sequence,
chainId: state.chain.chainId,
chainId: state!.chain.chainId as string,
};
const { bodyBytes, signatures } = await signingClient.sign(
+7 -1
View File
@@ -5,7 +5,13 @@ import { string } from "prop-types";
const defaultDescription = "Create multisigs and send tokens on any cosmos based chain";
const defaultOGURL = "";
const Head = (props) => (
interface Props {
title?: string;
description?: string;
url?: string;
}
const Head = (props: Props) => (
<NextHead>
<meta charSet="UTF-8" />
<title>{props.title || ""}</title>
@@ -1,6 +1,9 @@
import React from "react";
const Gear = (props) => (
interface Props {
color: string;
}
const Gear = (props: Props) => (
<i>
<svg viewBox="0 0 640 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -1,13 +1,16 @@
import React from "react";
const Button = (props) => (
interface Props {
primary?: boolean;
disabled?: boolean;
href?: string;
label: string;
onClick?: () => any;
}
const Button = (props: Props) => (
<>
{props.href ? (
<a
className={props.primary ? "primary button" : "button"}
href={props.href}
disabled={props.disabled}
>
<a className={props.primary ? "primary button" : "button"} href={props.href}>
{props.label}
</a>
) : (
@@ -1,6 +1,19 @@
import React from "react";
const Input = (props) => (
interface Props {
label?: string;
type?: string;
name?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: number | string | undefined;
onBlur?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
error?: string;
placeholder?: string;
width?: string;
}
const Input = (props: Props) => (
<div className="text-input">
<label>{props.label || ""}</label>
<input
@@ -1,9 +1,9 @@
import React from "react";
import Select from "react-select";
import React, { CSSProperties } from "react";
import Select, { ControlProps } from "react-select";
const StyledSelect = (props) => {
const StyledSelect = (props: any) => {
const customStyles = {
control: (provided, state) => ({
control: (provided: CSSProperties, state: ControlProps) => ({
...provided,
borderRadius: "10px",
background: "none",
@@ -16,7 +16,7 @@ const StyledSelect = (props) => {
borderColor: "rgba(255, 255, 255, 1)",
},
}),
option: (provided, state) => ({
option: (provided: CSSProperties, state: any) => ({
...provided,
background: state.isSelected ? "rgba(255, 255, 255, 0.2)" : "none",
color: "white",
@@ -25,25 +25,25 @@ const StyledSelect = (props) => {
background: "rgba(255, 255, 255, 0.2)",
},
}),
menu: (provided) => ({
menu: (provided: CSSProperties) => ({
...provided,
zIndex: 10,
borderRadius: "10px",
background: "#561253",
}),
singleValue: (provided) => ({
singleValue: (provided: CSSProperties) => ({
...provided,
color: "white",
}),
input: (provided) => ({
input: (provided: CSSProperties) => ({
...provided,
color: "white",
}),
placeholder: (provided) => ({
placeholder: (provided: CSSProperties) => ({
...provided,
color: "rgba(255,255,255, 0.6)",
}),
dropdownIndicator: (provided) => ({
dropdownIndicator: (provided: CSSProperties) => ({
...provided,
color: "rgba(255, 255, 255, 0.6)",
"&:hover": {
@@ -1,6 +1,12 @@
import React from "react";
const ThresholdInput = (props) => (
interface Props {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: number;
total: number;
}
const ThresholdInput = (props: Props) => (
<>
<p>Signatures required to send a transaction</p>
<div className="threshold-group">
@@ -3,7 +3,13 @@ import React from "react";
import Head from "../head";
import StackableContainer from "./StackableContainer";
const Page = (props) => {
interface Props {
title?: string;
rootMultisig?: string;
children: React.ReactNode;
}
const Page = (props: Props) => {
return (
<div className="page">
<Head title={props.title || "Cosmos Multisig Manager"} />
@@ -1,6 +1,15 @@
import React from "react";
const StackableContainer = (props) => (
interface Props {
base?: boolean;
children: React.ReactNode;
lessPadding?: boolean;
lessMargin?: boolean;
lessRadius?: boolean;
fullHeight?: boolean;
}
const StackableContainer = (props: Props) => (
<div className={`container ${props.base ? "base" : ""}`}>
{props.children}
-63
View File
@@ -1,63 +0,0 @@
import React, { useEffect, createContext, useContext, useReducer } from "react";
import { AppReducer, initialState } from "./AppReducer";
const AppContext = createContext();
function getChainInfoFromUrl() {
const url = location.search;
const params = new URLSearchParams(url);
const chainInfo = {
nodeAddress: decodeURIComponent(params.get("nodeAddress")),
denom: params.get("denom"),
displayDenom: params.get("displayDenom"),
displayDenomExponent: parseInt(params.get("displayDenomExponent"), 10),
gasPrice: params.get("gasPrice"),
chainId: params.get("chainId"),
chainDisplayName: decodeURIComponent(params.get("chainDisplayName")),
registryName: params.get("registryName"),
addressPrefix: params.get("addressPrefix"),
explorerLink: decodeURIComponent(params.get("explorerLink")),
};
return chainInfo;
}
function setChainInfoParams(chainInfo) {
const params = new URLSearchParams();
Object.keys(chainInfo).forEach((key) => {
params.set(key, encodeURIComponent(chainInfo[key]));
});
window.history.replaceState({}, "", `${location.pathname}?${params}`);
}
export function AppWrapper({ children }) {
let existingState;
if (typeof window !== "undefined") {
existingState = JSON.parse(localStorage.getItem("state"));
const urlChainInfo = getChainInfoFromUrl();
// query params should override saved state
if (urlChainInfo.chainId) {
console.log("setting state from url");
existingState = { chain: urlChainInfo };
}
}
const [state, dispatch] = useReducer(AppReducer, existingState ? existingState : initialState);
const contextValue = { state, dispatch };
useEffect(() => {
if (state && state !== initialState) {
localStorage.setItem("state", JSON.stringify(state));
setChainInfoParams(state.chain);
}
}, [state]);
return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>;
}
export function useAppContext() {
return useContext(AppContext);
}
+77
View File
@@ -0,0 +1,77 @@
import React, { useEffect, createContext, useContext, useReducer } from "react";
import { AppReducer, ChangeChainAction, initialState } from "./AppReducer";
import { ChainInfo } from "../types";
export interface AppContextType {
chain: ChainInfo;
}
const AppContext = createContext<{
state: AppContextType;
dispatch: React.Dispatch<ChangeChainAction>;
}>({ state: initialState, dispatch: () => {} });
function getChainInfoFromUrl(): ChainInfo {
const url = location.search;
const params = new URLSearchParams(url);
const chainInfo: ChainInfo = {
nodeAddress: decodeURIComponent(params.get("nodeAddress") || ""),
denom: params.get("denom") || "",
displayDenom: params.get("displayDenom") || "",
displayDenomExponent: parseInt(params.get("displayDenomExponent") || "", 10),
gasPrice: params.get("gasPrice") || "",
chainId: params.get("chainId") || "",
chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""),
registryName: params.get("registryName") || "",
addressPrefix: params.get("addressPrefix") || "",
explorerLink: decodeURIComponent(params.get("explorerLink") || ""),
};
return chainInfo;
}
function setChainInfoParams(chainInfo: ChainInfo) {
const params = new URLSearchParams();
const keys = Object.keys(chainInfo) as Array<keyof ChainInfo>;
keys.forEach((value: keyof ChainInfo) => {
params.set(value, encodeURIComponent(chainInfo[value] || ""));
});
window.history.replaceState({}, "", `${location.pathname}?${params}`);
}
export function AppWrapper({ children }: { children: React.ReactNode }) {
let existingState;
if (typeof window !== "undefined") {
const storedState = localStorage.getItem("state");
if (storedState) {
existingState = JSON.parse(storedState);
}
const urlChainInfo = getChainInfoFromUrl();
// query params should override saved state
if (urlChainInfo.chainId) {
console.log("setting state from url");
existingState = { chain: urlChainInfo };
}
}
const [state, dispatch] = useReducer(AppReducer, existingState ? existingState : initialState);
const contextValue = { state, dispatch };
useEffect(() => {
if (state && state !== initialState) {
localStorage.setItem("state", JSON.stringify(state));
setChainInfoParams(state.chain);
}
}, [state]);
return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>;
}
export function useAppContext() {
return useContext(AppContext);
}
@@ -1,9 +1,12 @@
export const initialState = {
import { ChainInfo } from "../types";
import { AppContextType } from "./AppContext";
export const initialState: AppContextType = {
chain: {
nodeAddress: process.env.NEXT_PUBLIC_NODE_ADDRESS,
denom: process.env.NEXT_PUBLIC_DENOM,
displayDenom: process.env.NEXT_PUBLIC_DISPLAY_DENOM,
displayDenomExponent: parseInt(process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT, 10),
displayDenomExponent: parseInt(process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT || "", 10),
gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE,
chainId: process.env.NEXT_PUBLIC_CHAIN_ID,
chainDisplayName: process.env.NEXT_PUBLIC_CHAIN_DISPLAY_NAME,
@@ -13,7 +16,12 @@ export const initialState = {
},
};
export const AppReducer = (state, action) => {
export interface ChangeChainAction {
type: "changeChain";
value: ChainInfo;
}
export const AppReducer = (state: AppContextType, action: ChangeChainAction) => {
switch (action.type) {
case "changeChain": {
return {
@@ -1,6 +1,8 @@
import { Bech32, fromBase64, toBase64 } from "@cosmjs/encoding";
import { sha512 } from "@cosmjs/crypto";
import { Decimal } from "@cosmjs/math";
import { Coin } from "@cosmjs/amino";
import { ChainInfo } from "../types";
/**
* Abbreviates long strings, typically used for
@@ -9,7 +11,7 @@ import { Decimal } from "@cosmjs/math";
* @param {string} longString The string to abbreviate.
* @return {string} The abbreviated string.
*/
const abbreviateLongString = (longString) => {
const abbreviateLongString = (longString: string) => {
if (longString.length < 13) {
// no need to abbreviate
return longString;
@@ -34,7 +36,7 @@ const thinSpace = "\u202F";
* @param {object} chainInfo Provides information about a chain (e.g. node, prefix, denomination), object structure defined in '../context/AppReducer'.
* @return {string} The abbreviated string.
*/
const printableCoin = (coin, chainInfo) => {
const printableCoin = (coin: Coin, chainInfo: ChainInfo) => {
// null, undefined and this sort of things
if (!coin) return "";
@@ -57,7 +59,7 @@ const printableCoin = (coin, chainInfo) => {
return coin.amount + thinSpace + coin.denom;
};
const printableCoins = (coins, chainInfo) => {
const printableCoins = (coins: Coin[], chainInfo: ChainInfo) => {
if (coins.length !== 1) {
throw new Error("Implementation only supports exactly one coin entry.");
}
@@ -69,7 +71,7 @@ const printableCoins = (coins, chainInfo) => {
*
* `index` can be set to a small integer in order to get different addresses. Defaults to 0.
*/
const exampleAddress = (index, chainAddressPrefix) => {
const exampleAddress = (index: number, chainAddressPrefix: string) => {
const usedIndex = index || 0;
let data = Bech32.decode("cosmos1vqpjljwsynsn58dugz0w8ut7kun7t8ls2qkmsq").data;
for (let i = 0; i < usedIndex; ++i) {
@@ -86,7 +88,7 @@ const exampleAddress = (index, chainAddressPrefix) => {
* Note: the keys are not necessarily valid (as in points on the chain) and should only be used
* as dummy data.
*/
const examplePubkey = (index) => {
const examplePubkey = (index: number) => {
const usedIndex = index || 0;
let data = fromBase64("Akd/qKMWdZXyiMnSu6aFLpQEGDO0ijyal9mXUIcVaPNX");
for (let i = 0; i < usedIndex; ++i) {
@@ -101,14 +103,14 @@ const examplePubkey = (index) => {
*
* Returns null of there is no error.
*/
const checkAddress = (input, chainAddressPrefix) => {
const checkAddress = (input: string, chainAddressPrefix: string) => {
if (!input) return "Empty";
let data;
let prefix;
try {
({ data, prefix } = Bech32.decode(input));
} catch (error) {
} catch (error: any) {
return error.toString();
}
@@ -127,7 +129,7 @@ const checkAddress = (input, chainAddressPrefix) => {
* Returns a link to a transaction in an explorer if an explorer is configured
* for transactions. Returns null otherwise.
*/
const explorerLinkTx = (link, hash) => {
const explorerLinkTx = (link: string, hash: string) => {
if (link && link.includes("${txHash}")) {
return link.replace("${txHash}", hash);
}
@@ -1,4 +1,5 @@
import axios from "axios";
import { DbAccount, DbSignature, DbTransaction } from "../types";
// Graphql base request for Faunadb
const graphqlReq = axios.create({
@@ -14,7 +15,7 @@ const graphqlReq = axios.create({
* @param {object} multisig an object with address (string), pubkey JSON and chainId
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const createMultisig = async (multisig) => {
const createMultisig = async (multisig: DbAccount) => {
console.log(multisig);
return graphqlReq({
method: "POST",
@@ -43,7 +44,7 @@ const createMultisig = async (multisig) => {
* @param {string} chainId The chainId the multisig belongs to.
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const getMultisig = async (address, chainId) => {
const getMultisig = async (address: string, chainId: string) => {
return graphqlReq({
method: "POST",
data: {
@@ -66,7 +67,7 @@ const getMultisig = async (address, chainId) => {
* @param {object} transaction The base transaction
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const createTransaction = async (transaction) => {
const createTransaction = async (transaction: DbTransaction) => {
return graphqlReq({
method: "POST",
data: {
@@ -87,7 +88,7 @@ const createTransaction = async (transaction) => {
* @param {string} id Faunadb resource id
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const findTransactionByID = async (id) => {
const findTransactionByID = async (id: string) => {
return graphqlReq({
method: "POST",
data: {
@@ -118,7 +119,7 @@ const findTransactionByID = async (id) => {
* @param {string} txHash tx hash returned from broadcasting a tx
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const updateTxHash = async (id, txHash) => {
const updateTxHash = async (id: string, txHash: string) => {
return graphqlReq({
method: "POST",
data: {
@@ -149,7 +150,7 @@ const updateTxHash = async (id, txHash) => {
* @param {string} transactionId id of the transaction to relate the signature with
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const createSignature = async (signature, transactionId) => {
const createSignature = async (signature: DbSignature, transactionId: string) => {
return graphqlReq({
method: "POST",
data: {
@@ -1,5 +1,7 @@
import axios from "axios";
import { createMultisigThresholdPubkey, pubkeyToAddress } from "@cosmjs/amino";
import { Account } from "@cosmjs/stargate";
import { StargateClient } from "@cosmjs/stargate";
/**
* Turns array of compressed Secp256k1 pubkeys
@@ -12,11 +14,11 @@ import { createMultisigThresholdPubkey, pubkeyToAddress } from "@cosmjs/amino";
* @return {string} The multisig address.
*/
const createMultisigFromCompressedSecp256k1Pubkeys = async (
compressedPubkeys,
threshold,
addressPrefix,
chainId,
) => {
compressedPubkeys: string[],
threshold: number,
addressPrefix: string,
chainId: string,
): Promise<string> => {
const pubkeys = compressedPubkeys.map((compressedPubkey) => {
return {
type: "tendermint/PubKeySecp256k1",
@@ -46,12 +48,15 @@ const createMultisigFromCompressedSecp256k1Pubkeys = async (
* @param client A connected stargate cosmoshub client
* @return {object} The multisig account.
*/
const getMultisigAccount = async (address, client) => {
const getMultisigAccount = async (
address: string,
client: StargateClient,
): Promise<Account | null> => {
// we need the multisig pubkeys to create transactions, if the multisig
// is new, and has never submitted a transaction its pubkeys will not be
// available from a node. If the multisig was created with this instance
// of this tool its pubkey will be available in the fauna datastore
let accountOnChain = await client.getAccount(address);
let accountOnChain: Mutable<Account> | null = await client.getAccount(address);
const chainId = await client.getChainId();
if (!accountOnChain || !accountOnChain.pubkey) {
@@ -64,11 +69,15 @@ const getMultisigAccount = async (address, client) => {
const pubkey = JSON.parse(res.data.pubkeyJSON);
if (!accountOnChain) {
accountOnChain = {};
accountOnChain = null;
}
accountOnChain.pubkey = pubkey;
accountOnChain!.pubkey = pubkey;
}
return accountOnChain;
};
type Mutable<Type> = {
-readonly [Key in keyof Type]: Type[Key];
};
export { createMultisigFromCompressedSecp256k1Pubkeys, getMultisigAccount };
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+556 -3512
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -19,19 +19,24 @@
"@ledgerhq/hw-transport-webusb": "^6.24.1",
"axios": "^0.21.1",
"copy-to-clipboard": "^3.3.1",
"cosmjs-types": "^0.4.1",
"faunadb": "^4.1.1",
"next": "^10.0.8",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"next": "^12.1.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-select": "^5.2.2",
"uuid": "^8.3.0"
},
"devDependencies": {
"@types/node": "^17.0.35",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@types/react-select": "^5.0.1",
"cosmjs-types": "^0.4.1",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.28.0",
"prettier": "^2.5.1"
"prettier": "^2.5.1",
"typescript": "^4.7.2"
}
}
+3 -2
View File
@@ -1,11 +1,12 @@
import React from "react";
import { AppWrapper } from "../context/AppContext";
import ChainSelect from "../components/chainSelect/ChainSelect";
import type { AppProps } from "next/app";
function MultisigApp({ Component, pageProps }) {
function MultisigApp({ Component, pageProps }: AppProps) {
return (
<AppWrapper>
{process.env.NEXT_PUBLIC_MULTICHAIN.toLowerCase() === "true" && <ChainSelect />}
{process.env.NEXT_PUBLIC_MULTICHAIN!.toLowerCase() === "true" && <ChainSelect />}
<Component {...pageProps} />
</AppWrapper>
);
@@ -1,10 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getMultisig } from "../../../../../../lib/graphqlHelpers";
export default async function (req, res) {
export default async function (req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "GET":
try {
const { multisigAddress, chainId } = req.query;
const multisigAddress = req.query.multisigAddress.toString();
const chainId = req.query.multisigAddress.toString();
console.log("Function `getMultisig` invoked", multisigAddress, chainId);
const getRes = await getMultisig(multisigAddress, chainId);
if (!getRes.data.data.getMultisig) {
@@ -14,7 +16,7 @@ export default async function (req, res) {
console.log("success", getRes.data.data.getMultisig);
res.status(200).send(getRes.data.data.getMultisig);
return;
} catch (err) {
} catch (err: any) {
console.log(err);
res.status(400).send(err.message);
return;
@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createMultisig } from "../../../../../lib/graphqlHelpers";
export default async function (req, res) {
export default async function (req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
@@ -10,7 +11,7 @@ export default async function (req, res) {
console.log("success", saveRes.data);
res.status(200).send(saveRes.data.data.createMultisig);
return;
} catch (err) {
} catch (err: any) {
console.log(err);
res.status(400).send(err.message);
return;
@@ -1,17 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { updateTxHash } from "../../../../lib/graphqlHelpers";
export default async function (req, res) {
export default async function (req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
const { transactionID } = req.query;
const transactionID = req.query.transactionID.toString();
const { txHash } = req.body;
console.log("Function `updateTransaction` invoked", txHash);
const saveRes = await updateTxHash(transactionID, txHash);
console.log("success", saveRes.data);
res.status(200).send(saveRes.data.data.updateTransaction);
return;
} catch (err) {
} catch (err: any) {
console.log(err);
res.status(400).send(err.message);
return;
@@ -1,17 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createSignature } from "../../../../lib/graphqlHelpers";
export default async function (req, res) {
export default async function (req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
const { transactionID } = req.query;
const transactionID = req.query.transactionID.toString();
const data = req.body;
console.log("Function `createSignature` invoked", data);
const saveRes = await createSignature(data, transactionID);
console.log("success", saveRes.data);
res.status(200).send(saveRes.data.data.createSignature);
return;
} catch (err) {
} catch (err: any) {
console.log(err);
res.status(400).send(err.message);
return;
@@ -1,6 +1,7 @@
import { createTransaction } from "../../../lib/graphqlHelpers";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function (req, res) {
export default async function (req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
@@ -10,7 +11,7 @@ export default async function (req, res) {
console.log("success", saveRes.data);
res.status(200).send({ transactionID: saveRes.data.data.createTransaction._id });
return;
} catch (err) {
} catch (err: any) {
console.log(err);
res.status(400).send(err.message);
return;
View File
@@ -1,6 +1,8 @@
import React, { useState, useEffect } from "react";
import { pubkeyToAddress } from "@cosmjs/amino";
import { pubkeyToAddress, Pubkey } from "@cosmjs/amino";
import { Account } from "@cosmjs/stargate";
import { StargateClient } from "@cosmjs/stargate";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { useRouter } from "next/router";
import { useAppContext } from "../../../context/AppContext";
@@ -13,39 +15,44 @@ import Page from "../../../components/layout/Page";
import StackableContainer from "../../../components/layout/StackableContainer";
import TransactionForm from "../../../components/forms/TransactionForm";
function participantPubkeysFromMultisig(multisigPubkey) {
function participantPubkeysFromMultisig(multisigPubkey: Pubkey) {
return multisigPubkey.value.pubkeys;
}
function participantAddressesFromMultisig(multisigPubkey, addressPrefix) {
return participantPubkeysFromMultisig(multisigPubkey).map((p) =>
function participantAddressesFromMultisig(multisigPubkey: Pubkey, addressPrefix: string) {
return participantPubkeysFromMultisig(multisigPubkey).map((p: Pubkey) =>
pubkeyToAddress(p, addressPrefix),
);
}
const multipage = (_props) => {
interface Props {}
const multipage = (_props: Props) => {
const { state } = useAppContext();
const [showTxForm, setShowTxForm] = useState(false);
const [holdings, setHoldings] = useState("");
const [accountOnChain, setAccountOnChain] = useState(null);
const [holdings, setHoldings] = useState<Coin | null>(null);
const [multisigAddress, setMultisigAddress] = useState("");
const [accountOnChain, setAccountOnChain] = useState<Account | null>(null);
const [accountError, setAccountError] = useState(null);
const router = useRouter();
useEffect(() => {
if (router.query.address) {
fetchMultisig(router.query.address);
const address = router.query.address?.toString();
if (address) {
setMultisigAddress(address);
fetchMultisig(address);
}
}, [state, router.query.address]);
const fetchMultisig = async (address) => {
const fetchMultisig = async (address: string) => {
setAccountError(null);
try {
const client = await StargateClient.connect(state.chain.nodeAddress);
const tempHoldings = await client.getBalance(address, state.chain.denom);
const client = await StargateClient.connect(state!.chain.nodeAddress!);
const tempHoldings = await client.getBalance(address, state!.chain.denom!);
const tempAccountOnChain = await getMultisigAccount(address, client);
setHoldings(tempHoldings);
setAccountOnChain(tempAccountOnChain);
} catch (error) {
} catch (error: any) {
setAccountError(error.message);
console.log("Account error:", error);
}
@@ -57,14 +64,18 @@ const multipage = (_props) => {
<StackableContainer>
<label>Multisig Address</label>
<h1>
<HashView hash={router.query.address} />
{router.query.address ? (
<HashView hash={router.query.address?.toString()} />
) : (
"No Address"
)}
</h1>
</StackableContainer>
{accountOnChain?.pubkey && (
<MultisigMembers
members={participantAddressesFromMultisig(
accountOnChain?.pubkey,
state.chain.addressPrefix,
state!.chain.addressPrefix!,
)}
threshold={accountOnChain?.pubkey.value.threshold}
/>
@@ -86,7 +97,7 @@ const multipage = (_props) => {
)}
{showTxForm ? (
<TransactionForm
address={router.query.address}
address={multisigAddress}
accountOnChain={accountOnChain}
closeForm={() => {
setShowTxForm(false);
@@ -1,11 +1,15 @@
import React from "react";
import axios from "axios";
import { StargateClient, makeMultisignedTx } from "@cosmjs/stargate";
import React from "react";
import { GetServerSideProps } from "next";
import { StargateClient, makeMultisignedTx, Account } from "@cosmjs/stargate";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { fromBase64 } from "@cosmjs/encoding";
import { MultisigThresholdPubkey } from "@cosmjs/amino";
import { DbSignature, DbTransaction } from "../../../../types";
import { useAppContext } from "../../../../context/AppContext";
import Button from "../../../../components/inputs/Button";
import { findTransactionByID } from "../../../../lib/graphqlHelpers";
@@ -17,9 +21,18 @@ import TransactionInfo from "../../../../components/dataViews/TransactionInfo";
import TransactionSigning from "../../../../components/forms/TransactionSigning";
import CompletedTransaction from "../../../../components/dataViews/CompletedTransaction";
export async function getServerSideProps(context) {
interface Props {
props: {
transactionJSON: string;
transactionID: string;
txHash: string;
signatures: DbSignature[];
};
}
export const getServerSideProps: GetServerSideProps = async (context): Promise<Props> => {
// get transaction info
const transactionID = context.params.transactionID;
const transactionID = context.params!.transactionID!.toString();
let transactionJSON;
let txHash;
let signatures;
@@ -30,7 +43,7 @@ export async function getServerSideProps(context) {
txHash = getRes.data.data.findTransactionByID.txHash;
transactionJSON = getRes.data.data.findTransactionByID.dataJSON;
signatures = getRes.data.data.findTransactionByID.signatures.data || [];
} catch (err) {
} catch (err: any) {
console.log(err);
}
return {
@@ -41,7 +54,7 @@ export async function getServerSideProps(context) {
signatures,
},
};
}
};
const transactionPage = ({
multisigAddress,
@@ -49,36 +62,43 @@ const transactionPage = ({
transactionID,
signatures,
txHash,
}: {
multisigAddress: string;
transactionJSON: string;
transactionID: string;
signatures: DbSignature[];
txHash: string;
}) => {
const { state } = useAppContext();
const [currentSignatures, setCurrentSignatures] = useState(signatures);
const [broadcastError, setBroadcastError] = useState("");
const [isBroadcasting, setIsBroadcasting] = useState(false);
const [transactionHash, setTransactionHash] = useState(txHash);
const [_holdings, setHoldings] = useState("");
const [accountOnChain, setAccountOnChain] = useState(null);
const [_holdings, setHoldings] = useState<Coin | null>(null);
const [accountOnChain, setAccountOnChain] = useState<Account | null>(null);
const [accountError, setAccountError] = useState(null);
const txInfo = (transactionJSON && JSON.parse(transactionJSON)) || null;
const txInfo: DbTransaction = (transactionJSON && JSON.parse(transactionJSON)) || null;
const router = useRouter();
const addSignature = (signature) => {
setCurrentSignatures((prevState) => [...prevState, signature]);
const addSignature = (signature: DbSignature) => {
setCurrentSignatures((prevState: DbSignature[]) => [...prevState, signature]);
};
useEffect(() => {
if (router.query.address) {
fetchMultisig(router.query.address);
const address = router.query.address?.toString();
if (address) {
fetchMultisig(address);
}
}, [router.query.address]);
}, [state, router.query.address]);
const fetchMultisig = async (address) => {
const fetchMultisig = async (address: string) => {
try {
const client = await StargateClient.connect(state.chain.nodeAddress);
const tempHoldings = await client.getBalance(address, state.chain.denom);
const client = await StargateClient.connect(state!.chain.nodeAddress!);
const tempHoldings = await client.getBalance(address, state!.chain.denom!);
const tempAccountOnChain = await getMultisigAccount(address, client);
setHoldings(tempHoldings);
setAccountOnChain(tempAccountOnChain);
} catch (error) {
} catch (error: any) {
setAccountError(error.message);
console.log("Account error:", error);
}
@@ -91,13 +111,13 @@ const transactionPage = ({
const bodyBytes = fromBase64(currentSignatures[0].bodyBytes);
const signedTx = makeMultisignedTx(
accountOnChain.pubkey,
accountOnChain!.pubkey as MultisigThresholdPubkey,
txInfo.sequence,
txInfo.fee,
bodyBytes,
new Map(currentSignatures.map((s) => [s.address, fromBase64(s.signature)])),
);
const broadcaster = await StargateClient.connect(state.chain.nodeAddress);
const broadcaster = await StargateClient.connect(state!.chain.nodeAddress!);
const result = await broadcaster.broadcastTx(
Uint8Array.from(TxRaw.encode(signedTx).finish()),
);
@@ -106,7 +126,7 @@ const transactionPage = ({
txHash: result.transactionHash,
});
setTransactionHash(result.transactionHash);
} catch (e) {
} catch (e: any) {
setIsBroadcasting(false);
setBroadcastError(e.message);
}
@@ -131,7 +151,7 @@ const transactionPage = ({
<ThresholdInfo signatures={currentSignatures} account={accountOnChain} />
)}
{accountOnChain &&
currentSignatures.length >= parseInt(accountOnChain.pubkey.value.threshold, 10) &&
currentSignatures.length >= parseInt(accountOnChain!.pubkey!.value.threshold, 10) &&
!transactionHash && (
<>
<Button
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
+55
View File
@@ -0,0 +1,55 @@
import { StdFee } from "@cosmjs/amino";
import { EncodeObject } from "@cosmjs/proto-signing";
declare global {
interface Window {
keplr: {
enable: (chainId: string) => any;
getKey: (chainId: string) => Promise<WalletAccount>;
};
getOfflineSignerOnlyAmino: any;
}
}
export interface DbSignature {
bodyBytes: string;
signature: string;
address: string;
}
export interface DbTransaction {
accountNumber: number;
sequence: number;
chainId: string;
msgs: EncodeObject[];
fee: StdFee;
memo: string;
}
export interface DbAccount {
address: string;
pubkeyJSON: string;
chainId: string;
}
export interface WalletAccount {
address?: Uint8Array;
pubkey: Uint8Array;
algo: string;
bech32Address: string;
isNanoLedger?: boolean;
name?: string;
}
export interface ChainInfo {
nodeAddress?: string;
denom?: string;
displayDenom?: string;
displayDenomExponent?: number;
gasPrice?: string;
chainId?: string;
chainDisplayName?: string;
registryName?: string;
addressPrefix?: string;
explorerLink?: string;
}