Merge branch 'master' into fix/use-sig-crash
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
interface SpinnerProps {
|
||||
readonly size?: number;
|
||||
}
|
||||
|
||||
const Spinner = ({ size }: SpinnerProps) => (
|
||||
<>
|
||||
<div className="spinner"></div>
|
||||
<style jsx>{`
|
||||
.spinner {
|
||||
border: ${size || 2}px solid #f3f3f3;
|
||||
border-top: ${size || 2}px solid #561253;
|
||||
border-radius: 50%;
|
||||
width: ${size || 16}px;
|
||||
height: ${size || 16}px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
@@ -8,7 +8,12 @@ import Button from "../inputs/Button";
|
||||
import Input from "../inputs/Input";
|
||||
import Select from "../inputs/Select";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
import { ChainRegistryAsset } from "./chainregistry";
|
||||
import {
|
||||
RegistryChainApisRpc,
|
||||
RegistryChainExplorer,
|
||||
getAssetsFromRegistry,
|
||||
getChainFromRegistry,
|
||||
} from "./chainregistry";
|
||||
|
||||
interface ChainOption {
|
||||
label: string;
|
||||
@@ -112,76 +117,83 @@ const ChainSelect = () => {
|
||||
|
||||
const getChainInfo = async (chainOption: GithubChainRegistryItem) => {
|
||||
setChainError(null);
|
||||
|
||||
try {
|
||||
const chainInfoUrl =
|
||||
"https://cdn.jsdelivr.net/gh/cosmos/chain-registry@master/" +
|
||||
chainOption.path +
|
||||
"/chain.json";
|
||||
const chainAssetUrl =
|
||||
"https://cdn.jsdelivr.net/gh/cosmos/chain-registry@master/" +
|
||||
chainOption.path +
|
||||
"/assetlist.json";
|
||||
const chainData = await getChainFromRegistry(chainOption.path);
|
||||
const assets = await getAssetsFromRegistry(chainOption.path);
|
||||
const firstAsset = assets[0];
|
||||
|
||||
const { data: chainData } = await axios.get(chainInfoUrl);
|
||||
const { data: assetData } = await axios.get(chainAssetUrl);
|
||||
|
||||
const nodeAddress = getNodeFromArray(chainData.apis.rpc);
|
||||
const addressPrefix = chainData["bech32_prefix"];
|
||||
const chainId = chainData["chain_id"];
|
||||
const chainDisplayName = chainData["pretty_name"];
|
||||
const registryName = chainOption.name;
|
||||
const nodeAddress = await getNodeFromArray(chainData.apis.rpc);
|
||||
const explorerLink = getExplorerFromArray(chainData.explorers);
|
||||
const denom = firstAsset.base || "";
|
||||
const displayDenom = firstAsset.symbol || "";
|
||||
|
||||
const firstAsset: ChainRegistryAsset | undefined = assetData.assets?.[0];
|
||||
const denom = firstAsset?.base || "";
|
||||
const displayDenom = firstAsset?.symbol || "";
|
||||
const gasPrice = firstAsset ? `0.03${firstAsset.base}` : "";
|
||||
const displayUnit = firstAsset?.denom_units.find((u) => u.denom == firstAsset.display);
|
||||
const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display);
|
||||
const displayDenomExponent = displayUnit?.exponent ?? 6;
|
||||
|
||||
// test client connection
|
||||
const client = await StargateClient.connect(nodeAddress);
|
||||
await client.getHeight();
|
||||
const feeToken = chainData.fees.fee_tokens.find((token) => token.denom == denom) ?? { denom };
|
||||
const gasPrice =
|
||||
feeToken.average_gas_price ??
|
||||
feeToken.low_gas_price ??
|
||||
feeToken.high_gas_price ??
|
||||
feeToken.fixed_min_gas_price ??
|
||||
0.03;
|
||||
const formattedGasPrice = firstAsset ? `${gasPrice}${denom}` : "";
|
||||
|
||||
// change app state
|
||||
dispatch({
|
||||
type: "changeChain",
|
||||
value: {
|
||||
registryName: chainOption.name,
|
||||
addressPrefix: chainData.bech32_prefix,
|
||||
chainId: chainData.chain_id,
|
||||
chainDisplayName: chainData.pretty_name,
|
||||
nodeAddress,
|
||||
explorerLink,
|
||||
denom,
|
||||
displayDenom,
|
||||
displayDenomExponent,
|
||||
gasPrice,
|
||||
chainId,
|
||||
chainDisplayName,
|
||||
registryName,
|
||||
addressPrefix,
|
||||
explorerLink,
|
||||
gasPrice: formattedGasPrice,
|
||||
},
|
||||
});
|
||||
|
||||
setShowSettings(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setChainError(error.message);
|
||||
} else {
|
||||
setChainError("Error getting chain info");
|
||||
}
|
||||
|
||||
console.error("Error getting chain info", error);
|
||||
setShowSettings(true);
|
||||
setChainError(error.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const getExplorerFromArray = (array: { kind: string; url: string; tx_page: string }[]) => {
|
||||
if (array && array.length > 0) {
|
||||
return array[0]["tx_page"];
|
||||
}
|
||||
return "";
|
||||
const getExplorerFromArray = (explorers: readonly RegistryChainExplorer[]) => {
|
||||
return explorers[0]?.tx_page ?? "";
|
||||
};
|
||||
|
||||
const getNodeFromArray = (nodeArray: { address: string; provider: string }[]) => {
|
||||
const getNodeFromArray = async (nodeArray: readonly RegistryChainApisRpc[]) => {
|
||||
// only return https connections
|
||||
const secureNodes = nodeArray.filter((node) => node.address.includes("https://"));
|
||||
const secureNodes = nodeArray
|
||||
.filter((node) => node.address.startsWith("https://"))
|
||||
.map(({ address }) => address);
|
||||
|
||||
if (secureNodes.length === 0) {
|
||||
throw new Error("No SSL enabled RPC nodes available for this chain");
|
||||
}
|
||||
return secureNodes[0].address;
|
||||
|
||||
for (const node of secureNodes) {
|
||||
try {
|
||||
// test client connection
|
||||
const client = await StargateClient.connect(node);
|
||||
await client.getHeight();
|
||||
return node;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
throw new Error("No RPC nodes available for this chain");
|
||||
};
|
||||
|
||||
const onChainSelect = (option: ChainOption) => {
|
||||
|
||||
@@ -1,7 +1,49 @@
|
||||
import axios from "axios";
|
||||
|
||||
export interface RegistryChainApisRpc {
|
||||
readonly address: string;
|
||||
readonly provider: string;
|
||||
}
|
||||
|
||||
export interface RegistryChainApis {
|
||||
readonly rpc: readonly RegistryChainApisRpc[];
|
||||
}
|
||||
|
||||
export interface RegistryChainExplorer {
|
||||
readonly kind: string;
|
||||
readonly url: string;
|
||||
readonly tx_page: string;
|
||||
}
|
||||
|
||||
export interface RegistryChainFeeTokens {
|
||||
readonly denom: string;
|
||||
readonly average_gas_price?: number;
|
||||
readonly low_gas_price?: number;
|
||||
readonly high_gas_price?: number;
|
||||
readonly fixed_min_gas_price?: number;
|
||||
}
|
||||
|
||||
export interface RegistryChainFees {
|
||||
readonly fee_tokens: readonly RegistryChainFeeTokens[];
|
||||
}
|
||||
|
||||
export interface RegistryChain {
|
||||
readonly apis: RegistryChainApis;
|
||||
readonly bech32_prefix: string;
|
||||
readonly chain_id: string;
|
||||
readonly explorers: readonly RegistryChainExplorer[];
|
||||
readonly fees: RegistryChainFees;
|
||||
readonly pretty_name: string;
|
||||
}
|
||||
|
||||
export interface RegistryChainResponse {
|
||||
readonly data: RegistryChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L8-L12
|
||||
*/
|
||||
export interface ChainRegistryDemonUnit {
|
||||
export interface RegistryAssetDenomUnit {
|
||||
denom: string;
|
||||
exponent: number;
|
||||
aliases: string[];
|
||||
@@ -10,9 +52,9 @@ export interface ChainRegistryDemonUnit {
|
||||
/**
|
||||
* See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L5-L28
|
||||
*/
|
||||
export interface ChainRegistryAsset {
|
||||
export interface RegistryAsset {
|
||||
description: string;
|
||||
denom_units: ChainRegistryDemonUnit[];
|
||||
denom_units: RegistryAssetDenomUnit[];
|
||||
base: string;
|
||||
name: string;
|
||||
display: string;
|
||||
@@ -23,3 +65,28 @@ export interface ChainRegistryAsset {
|
||||
};
|
||||
coingecko_id: string;
|
||||
}
|
||||
|
||||
export interface RegistryAssetsResponse {
|
||||
readonly data: { readonly assets: readonly RegistryAsset[] };
|
||||
}
|
||||
|
||||
const registryGhUrl = "https://cdn.jsdelivr.net/gh/cosmos/chain-registry@master/";
|
||||
|
||||
export const getChainFromRegistry = async (chainGhName: string): Promise<RegistryChain> => {
|
||||
const chainGhUrl = registryGhUrl + chainGhName + "/chain.json";
|
||||
|
||||
const { data: chain }: RegistryChainResponse = await axios.get(chainGhUrl);
|
||||
return chain;
|
||||
};
|
||||
|
||||
export const getAssetsFromRegistry = async (
|
||||
chainGhName: string,
|
||||
): Promise<readonly RegistryAsset[]> => {
|
||||
const assetsGhUrl = registryGhUrl + chainGhName + "/assetlist.json";
|
||||
|
||||
const {
|
||||
data: { assets },
|
||||
}: RegistryAssetsResponse = await axios.get(assetsGhUrl);
|
||||
|
||||
return assets;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
import axios from "axios";
|
||||
import { NextRouter, withRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
import { checkAddress, exampleValidatorAddress } from "../../lib/displayHelpers";
|
||||
import Button from "../inputs/Button";
|
||||
@@ -24,7 +24,7 @@ const DelegationForm = (props: Props) => {
|
||||
const [memo, setMemo] = useState("");
|
||||
const [gas, setGas] = useState(200000);
|
||||
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [addressError, setAddressError] = useState("");
|
||||
|
||||
const createTransaction = (txValidatorAddress: string, txAmount: string, gasLimit: number) => {
|
||||
@@ -130,7 +130,7 @@ const DelegationForm = (props: Props) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button label="Delegate" onClick={handleCreate} />
|
||||
<Button label="Delegate" onClick={handleCreate} loading={processing} />
|
||||
<style jsx>{`
|
||||
p {
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { withRouter, NextRouter } from "next/router";
|
||||
import { StargateClient } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
|
||||
import { NextRouter, withRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
import Button from "../inputs/Button";
|
||||
import { createMultisigFromCompressedSecp256k1Pubkeys } from "../../lib/multisigHelpers";
|
||||
import Input from "../inputs/Input";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
import ThresholdInput from "../inputs/ThresholdInput";
|
||||
import { exampleAddress, examplePubkey } from "../../lib/displayHelpers";
|
||||
import { createMultisigFromCompressedSecp256k1Pubkeys } from "../../lib/multisigHelpers";
|
||||
import Button from "../inputs/Button";
|
||||
import Input from "../inputs/Input";
|
||||
import ThresholdInput from "../inputs/ThresholdInput";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
|
||||
const emptyPubKeyGroup = () => {
|
||||
return { address: "", compressedPubkey: "", keyError: "", isPubkey: false };
|
||||
@@ -23,7 +22,7 @@ const MultiSigForm = (props: Props) => {
|
||||
const { state } = useAppContext();
|
||||
const [pubkeys, setPubkeys] = useState([emptyPubKeyGroup(), emptyPubKeyGroup()]);
|
||||
const [threshold, setThreshold] = useState(2);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
const handleChangeThreshold = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let newThreshold = parseInt(e.target.value, 10);
|
||||
@@ -195,7 +194,7 @@ const MultiSigForm = (props: Props) => {
|
||||
</p>
|
||||
</StackableContainer>
|
||||
</StackableContainer>
|
||||
<Button primary onClick={handleCreate} label="Create Multisig" />
|
||||
<Button primary onClick={handleCreate} label="Create Multisig" loading={processing} />
|
||||
<style jsx>{`
|
||||
.key-inputs {
|
||||
display: flex;
|
||||
|
||||
@@ -25,7 +25,7 @@ const ReDelegationForm = (props: Props) => {
|
||||
const [memo, setMemo] = useState("");
|
||||
const [gas, setGas] = useState(300000);
|
||||
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [addressErrors, setAddressErrors] = useState({ src: "", dst: "" });
|
||||
|
||||
const createTransaction = (
|
||||
@@ -158,7 +158,7 @@ const ReDelegationForm = (props: Props) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button label="ReDelegate" onClick={handleCreate} />
|
||||
<Button label="ReDelegate" onClick={handleCreate} loading={processing} />
|
||||
<style jsx>{`
|
||||
p {
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -22,7 +22,7 @@ const RewardsForm = (props: Props) => {
|
||||
const [memo, setMemo] = useState("");
|
||||
const [gas, setGas] = useState(200000);
|
||||
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [addressError, setAddressError] = useState("");
|
||||
|
||||
const createTransaction = (txValidatorAddress: string, gasLimit: number) => {
|
||||
@@ -111,7 +111,7 @@ const RewardsForm = (props: Props) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button label="Claim Rewards" onClick={handleCreate} />
|
||||
<Button label="Claim Rewards" onClick={handleCreate} loading={processing} />
|
||||
<style jsx>{`
|
||||
p {
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import axios from "axios";
|
||||
import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
import { Decimal } from "@cosmjs/math";
|
||||
import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
import React, { useState } from "react";
|
||||
import { withRouter, NextRouter } from "next/router";
|
||||
|
||||
import axios from "axios";
|
||||
import { NextRouter, withRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
import { checkAddress, exampleAddress } from "../../lib/displayHelpers";
|
||||
import Button from "../inputs/Button";
|
||||
import Input from "../inputs/Input";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
import { checkAddress, exampleAddress } from "../../lib/displayHelpers";
|
||||
|
||||
interface Props {
|
||||
address: string | null;
|
||||
@@ -25,7 +24,7 @@ const TransactionForm = (props: Props) => {
|
||||
const [memo, setMemo] = useState("");
|
||||
const [gas, setGas] = useState(200000);
|
||||
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [addressError, setAddressError] = useState("");
|
||||
|
||||
const createTransaction = (txToAddress: string, txAmount: string, gasLimit: number) => {
|
||||
@@ -130,7 +129,7 @@ const TransactionForm = (props: Props) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button label="Create Transaction" onClick={handleCreate} />
|
||||
<Button label="Create Transaction" onClick={handleCreate} loading={processing} />
|
||||
<style jsx>{`
|
||||
p {
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
import { makeCosmoshubPath } from "@cosmjs/amino";
|
||||
import { toBase64 } from "@cosmjs/encoding";
|
||||
import { LedgerSigner } from "@cosmjs/ledger-amino";
|
||||
import { SigningStargateClient } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
|
||||
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";
|
||||
import axios from "axios";
|
||||
import { useState } from "react";
|
||||
import { useAppContext } from "../../context/AppContext";
|
||||
import { DbSignature, DbTransaction, WalletAccount } from "../../types";
|
||||
import HashView from "../dataViews/HashView";
|
||||
import Button from "../inputs/Button";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
|
||||
interface LoadingStates {
|
||||
readonly signing?: boolean;
|
||||
readonly keplr?: boolean;
|
||||
readonly ledger?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
signatures: DbSignature[];
|
||||
@@ -28,10 +32,13 @@ const TransactionSigning = (props: Props) => {
|
||||
const [hasSigned, setHasSigned] = useState(false);
|
||||
const [walletType, setWalletType] = useState<"Keplr" | "Ledger">();
|
||||
const [ledgerSigner, setLedgerSigner] = useState({});
|
||||
const [loading, setLoading] = useState<LoadingStates>({});
|
||||
|
||||
const connectKeplr = async () => {
|
||||
try {
|
||||
setLoading((oldLoading) => ({ ...oldLoading, keplr: true }));
|
||||
assert(state.chain.chainId, "chainId missing");
|
||||
|
||||
await window.keplr.enable(state.chain.chainId);
|
||||
window.keplr.defaultOptions = {
|
||||
sign: { preferNoSetFee: true, preferNoSetMemo: true, disableBalanceCheck: true },
|
||||
@@ -45,81 +52,102 @@ const TransactionSigning = (props: Props) => {
|
||||
setHasSigned(tempHasSigned);
|
||||
setWalletType("Keplr");
|
||||
} catch (e) {
|
||||
console.log("enable err: ", e);
|
||||
console.log("enable keplr err: ", e);
|
||||
} finally {
|
||||
setLoading((newLoading) => ({ ...newLoading, keplr: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const connectLedger = async () => {
|
||||
assert(state.chain.addressPrefix, "addressPrefix missing");
|
||||
try {
|
||||
setLoading((newLoading) => ({ ...newLoading, ledger: true }));
|
||||
assert(state.chain.addressPrefix, "addressPrefix missing");
|
||||
|
||||
// Prepare ledger
|
||||
const ledgerTransport = await TransportWebUSB.create(120000, 120000);
|
||||
// Prepare ledger
|
||||
const ledgerTransport = await TransportWebUSB.create(120000, 120000);
|
||||
|
||||
// Setup signer
|
||||
const offlineSigner = new LedgerSigner(ledgerTransport, {
|
||||
hdPaths: [makeCosmoshubPath(0)],
|
||||
prefix: state.chain.addressPrefix,
|
||||
});
|
||||
console.log(offlineSigner);
|
||||
const accounts = await offlineSigner.getAccounts();
|
||||
console.log(accounts);
|
||||
const tempWalletAccount: WalletAccount = {
|
||||
bech32Address: accounts[0].address,
|
||||
pubkey: accounts[0].pubkey,
|
||||
algo: accounts[0].algo,
|
||||
};
|
||||
// Setup signer
|
||||
const offlineSigner = new LedgerSigner(ledgerTransport, {
|
||||
hdPaths: [makeCosmoshubPath(0)],
|
||||
prefix: state.chain.addressPrefix,
|
||||
});
|
||||
console.log(offlineSigner);
|
||||
const accounts = await offlineSigner.getAccounts();
|
||||
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,
|
||||
);
|
||||
setWalletAccount(tempWalletAccount);
|
||||
setHasSigned(tempHasSigned);
|
||||
setLedgerSigner(offlineSigner);
|
||||
setWalletType("Ledger");
|
||||
const tempHasSigned = props.signatures.some(
|
||||
(sig) => sig.address === tempWalletAccount.bech32Address,
|
||||
);
|
||||
setWalletAccount(tempWalletAccount);
|
||||
setHasSigned(tempHasSigned);
|
||||
setLedgerSigner(offlineSigner);
|
||||
setWalletType("Ledger");
|
||||
} catch (e) {
|
||||
console.log("enable ledger err: ", e);
|
||||
} finally {
|
||||
setLoading((newLoading) => ({ ...newLoading, ledger: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const signTransaction = async () => {
|
||||
assert(state.chain.chainId, "chainId missing");
|
||||
try {
|
||||
setLoading((newLoading) => ({ ...newLoading, signing: true }));
|
||||
assert(state.chain.chainId, "chainId missing");
|
||||
|
||||
const offlineSigner =
|
||||
walletType === "Keplr" ? window.getOfflineSignerOnlyAmino(state.chain.chainId) : ledgerSigner;
|
||||
const offlineSigner =
|
||||
walletType === "Keplr"
|
||||
? window.getOfflineSignerOnlyAmino(state.chain.chainId)
|
||||
: ledgerSigner;
|
||||
|
||||
const signerAddress = walletAccount?.bech32Address;
|
||||
assert(signerAddress, "Missing signer address");
|
||||
const signingClient = await SigningStargateClient.offline(offlineSigner);
|
||||
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,
|
||||
};
|
||||
|
||||
const { bodyBytes, signatures } = await signingClient.sign(
|
||||
signerAddress,
|
||||
props.tx.msgs,
|
||||
props.tx.fee,
|
||||
props.tx.memo,
|
||||
signerData,
|
||||
);
|
||||
|
||||
// check existing signatures
|
||||
const bases64EncodedSignature = toBase64(signatures[0]);
|
||||
const bases64EncodedBodyBytes = toBase64(bodyBytes);
|
||||
const prevSigMatch = props.signatures.findIndex(
|
||||
(signature) => signature.signature === bases64EncodedSignature,
|
||||
);
|
||||
|
||||
if (prevSigMatch > -1) {
|
||||
setSigError("This account has already signed.");
|
||||
} else {
|
||||
const signature = {
|
||||
bodyBytes: bases64EncodedBodyBytes,
|
||||
signature: bases64EncodedSignature,
|
||||
address: signerAddress,
|
||||
const signerData = {
|
||||
accountNumber: props.tx.accountNumber,
|
||||
sequence: props.tx.sequence,
|
||||
chainId: state.chain.chainId,
|
||||
};
|
||||
const _res = await axios.post(`/api/transaction/${props.transactionID}/signature`, signature);
|
||||
props.addSignature(signature);
|
||||
setHasSigned(true);
|
||||
|
||||
const { bodyBytes, signatures } = await signingClient.sign(
|
||||
signerAddress,
|
||||
props.tx.msgs,
|
||||
props.tx.fee,
|
||||
props.tx.memo,
|
||||
signerData,
|
||||
);
|
||||
|
||||
// check existing signatures
|
||||
const bases64EncodedSignature = toBase64(signatures[0]);
|
||||
const bases64EncodedBodyBytes = toBase64(bodyBytes);
|
||||
const prevSigMatch = props.signatures.findIndex(
|
||||
(signature) => signature.signature === bases64EncodedSignature,
|
||||
);
|
||||
|
||||
if (prevSigMatch > -1) {
|
||||
setSigError("This account has already signed.");
|
||||
} else {
|
||||
const signature = {
|
||||
bodyBytes: bases64EncodedBodyBytes,
|
||||
signature: bases64EncodedSignature,
|
||||
address: signerAddress,
|
||||
};
|
||||
const _res = await axios.post(
|
||||
`/api/transaction/${props.transactionID}/signature`,
|
||||
signature,
|
||||
);
|
||||
props.addSignature(signature);
|
||||
setHasSigned(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("signing err: ", e);
|
||||
} finally {
|
||||
setLoading((newLoading) => ({ ...newLoading, signing: false }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,12 +172,20 @@ const TransactionSigning = (props: Props) => {
|
||||
Connected signer {walletAccount.bech32Address} (
|
||||
{walletType ?? "Unknown wallet type"}).
|
||||
</p>
|
||||
<Button label="Sign transaction" onClick={signTransaction} />
|
||||
<Button
|
||||
label="Sign transaction"
|
||||
onClick={signTransaction}
|
||||
loading={loading.signing}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button label="Connect Keplr" onClick={connectKeplr} />
|
||||
<Button label="Connect Ledger (WebUSB)" onClick={connectLedger} />
|
||||
<Button label="Connect Keplr" onClick={connectKeplr} loading={loading.keplr} />
|
||||
<Button
|
||||
label="Connect Ledger (WebUSB)"
|
||||
onClick={connectLedger}
|
||||
loading={loading.ledger}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StackableContainer>
|
||||
|
||||
@@ -24,7 +24,7 @@ const UnDelegationForm = (props: Props) => {
|
||||
const [memo, setMemo] = useState("");
|
||||
const [gas, setGas] = useState(300000);
|
||||
const [gasPrice, _setGasPrice] = useState(state.chain.gasPrice);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [addressError, setAddressError] = useState("");
|
||||
|
||||
const createTransaction = (txValidatorAddress: string, txAmount: string, gasLimit: number) => {
|
||||
@@ -130,7 +130,7 @@ const UnDelegationForm = (props: Props) => {
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button label="UnDelegate" onClick={handleCreate} />
|
||||
<Button label="UnDelegate" onClick={handleCreate} loading={processing} />
|
||||
<style jsx>{`
|
||||
p {
|
||||
margin-top: 15px;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import Spinner from "../Spinner";
|
||||
|
||||
interface Props {
|
||||
primary?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
label: string;
|
||||
@@ -17,10 +18,18 @@ const Button = (props: Props) => (
|
||||
) : (
|
||||
<button
|
||||
className={props.primary ? "primary button" : "button"}
|
||||
onClick={props.onClick}
|
||||
onClick={props.disabled || props.loading ? () => {} : props.onClick}
|
||||
disabled={props.disabled}
|
||||
data-loading={props.loading}
|
||||
>
|
||||
{props.label}
|
||||
{props.loading ? (
|
||||
<div className="button-cluster">
|
||||
<Spinner />
|
||||
{props.label}
|
||||
</div>
|
||||
) : (
|
||||
props.label
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<style jsx>{`
|
||||
@@ -44,10 +53,17 @@ const Button = (props: Props) => (
|
||||
button:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
button:disabled {
|
||||
button:disabled,
|
||||
button[data-loading="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: initial;
|
||||
}
|
||||
.button-cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user