Merge branch 'master' into feat/undelegate-redelegate-claim

This commit is contained in:
abefernan
2023-04-12 16:17:56 +02:00
26 changed files with 4870 additions and 2994 deletions
+1
View File
@@ -1,4 +1,5 @@
FAUNADB_SECRET=
FAUNADB_URL=https://graphql.eu.fauna.com/graphql
NEXT_PUBLIC_NODE_ADDRESS=https://cosmoshub.validator.network:443
NEXT_PUBLIC_DENOM=uatom
NEXT_PUBLIC_DISPLAY_DENOM=ATOM
+13
View File
@@ -0,0 +1,13 @@
FAUNADB_SECRET=
FAUNADB_URL=https://graphql.eu.fauna.com/graphql
NEXT_PUBLIC_NODE_ADDRESS=https://rpc.uni.junonetwork.io:443
NEXT_PUBLIC_DENOM=ujunox
NEXT_PUBLIC_DISPLAY_DENOM=JUNOX
NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT=6
NEXT_PUBLIC_GAS_PRICE=0.04ujunox
NEXT_PUBLIC_CHAIN_ID=uni-6
NEXT_PUBLIC_ADDRESS_PREFIX=juno
NEXT_PUBLIC_REGISTRY_NAME=junotestnet
NEXT_PUBLIC_EXPLORER_LINK_TX="https://testnet.mintscan.io/juno-testnet/txs/\${txHash}"
NEXT_PUBLIC_CHAIN_DISPLAY_NAME="Juno Testnet"
NEXT_PUBLIC_MULTICHAIN=true
+2 -9
View File
@@ -15,14 +15,8 @@ module.exports = {
jsx: true,
},
},
plugins: ["prettier", "@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"prettier",
"plugin:prettier/recommended",
],
plugins: ["@typescript-eslint"],
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "prettier"],
rules: {
curly: ["warn", "multi-line", "consistent"],
"no-bitwise": "warn",
@@ -41,5 +35,4 @@ module.exports = {
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
overrides: [],
};
+4 -1
View File
@@ -21,4 +21,7 @@ yarn-debug.log*
yarn-error.log*
# Local Netlify folder
.netlify
.netlify
# IDE folder
.idea
+1 -1
View File
@@ -52,7 +52,7 @@ This app relies on FaunaDB as for storing account, transaction and signature det
- Create a [FaunaDB](https://dashboard.fauna.com/) account
- Create a new database
- Use the "Classic" region
- Use the "Europe (EU)" region
- Click the "Graphql" tab, and import the `db-schema.graphql` file in the root of this repo
- Click the "Security" tab, and create a key. Copy that key into the `.env.local` file for the `FAUNADB_SECRET` value
+7 -7
View File
@@ -1,4 +1,4 @@
import React from "react";
import Link from "next/link";
const DevHelper = () => (
<div className="dev-helper">
@@ -6,21 +6,21 @@ const DevHelper = () => (
<h4>Pages</h4>
<ul>
<li>
<a href="/">Index/Start</a>
<Link href="/">Index/Start</Link>
</li>
<li>
<a href="/create">Create Multisig</a>
<Link href="/create">Create Multisig</Link>
</li>
<li>
<a href="/multi/cosmos10nmdf6nt2qzvgn9q2nuwmmfc359yfesmu3gw22">View Multisig</a>
<Link href="/multi/cosmos10nmdf6nt2qzvgn9q2nuwmmfc359yfesmu3gw22">View Multisig</Link>
</li>
<li>
<a href="/multi/cosmos10nmdf6nt2qzvgn9q2nuwmmfc359yfesmu3gw22/transaction/295630000375202310">
<Link href="/multi/cosmos10nmdf6nt2qzvgn9q2nuwmmfc359yfesmu3gw22/transaction/295630000375202310">
View/Sign Transaction
</a>
</Link>
</li>
</ul>
<a href="https://github.com/samepant/cosmoshub-legacy-multisig">View on Github</a>
<Link href="https://github.com/samepant/cosmoshub-legacy-multisig">View on Github</Link>
<style jsx>{`
.dev-helper {
+38 -32
View File
@@ -1,14 +1,14 @@
import React, { useEffect, useState } from "react";
import axios from "axios";
import { StargateClient } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useAppContext } from "../../context/AppContext";
import GearIcon from "../icons/Gear";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import { useAppContext } from "../../context/AppContext";
import Select from "../inputs/Select";
import StackableContainer from "../layout/StackableContainer";
import { assert } from "@cosmjs/utils";
import { ChainRegistryAsset } from "./chainregistry";
interface ChainOption {
label: string;
@@ -58,25 +58,7 @@ const ChainSelect = () => {
const url = "https://api.github.com/repos/cosmos/chain-registry/contents";
useEffect(() => {
getGhJson();
}, []);
useEffect(() => {
// set settings form fields to new values
setChainId(state.chain.chainId);
setNodeAddress(state.chain.nodeAddress);
setAddressPrefix(state.chain.addressPrefix);
setDenom(state.chain.denom);
setDisplayDenom(state.chain.displayDenom);
setDisplayDenomExponent(state.chain.displayDenomExponent);
setGasPrice(state.chain.gasPrice);
setChainName(state.chain.chainDisplayName);
setExplorerLink(state.chain.explorerLink);
setRegistryName(state.chain.registryName);
}, [state]);
const getGhJson = async () => {
const getGhJson = useCallback(async () => {
// getting chain info from this repo: https://github.com/cosmos/chain-registry
try {
const res = await axios.get(url);
@@ -96,14 +78,36 @@ const ChainSelect = () => {
setShowSettings(true);
setChainError(error.message);
}
};
}, [state.chain.registryName]);
useEffect(() => {
getGhJson();
}, [getGhJson]);
useEffect(() => {
// set settings form fields to new values
setChainId(state.chain.chainId);
setNodeAddress(state.chain.nodeAddress);
setAddressPrefix(state.chain.addressPrefix);
setDenom(state.chain.denom);
setDisplayDenom(state.chain.displayDenom);
setDisplayDenomExponent(state.chain.displayDenomExponent);
setGasPrice(state.chain.gasPrice);
setChainName(state.chain.chainDisplayName);
setExplorerLink(state.chain.explorerLink);
setRegistryName(state.chain.registryName);
}, [state]);
const findExistingOption = (options: ChainOption[], registryName: string) => {
const index = options.findIndex((option) => option.label === registryName);
if (index >= 0) {
return options[index];
}
return { label: "unkown chain", value: -1 };
return {
label:
registryName === process.env.NEXT_PUBLIC_REGISTRY_NAME ? registryName : "unknown chain",
value: -1,
};
};
const getChainInfo = async (chainOption: GithubChainRegistryItem) => {
@@ -127,24 +131,26 @@ const ChainSelect = () => {
const chainDisplayName = chainData["pretty_name"];
const registryName = chainOption.name;
const explorerLink = getExplorerFromArray(chainData.explorers);
let asset = null;
let denom = "";
let displayDenom = "";
const displayDenomExponent = 6;
let gasPrice = "";
let denom: string;
let displayDenom: string;
let displayDenomExponent: number;
let gasPrice: string;
if (assetData.assets.length > 1) {
denom = "";
displayDenom = "";
gasPrice = "";
displayDenomExponent = 0;
setChainError("Multiple token denoms available, enter manually");
setShowSettings(true);
} else {
asset = assetData.assets[0];
const asset: ChainRegistryAsset = assetData.assets[0];
denom = asset.base;
displayDenom = asset.symbol;
gasPrice = `0.03${asset.base}`;
const displayUnit = asset.denom_units.find((u) => u.denom == asset.display);
displayDenomExponent = displayUnit?.exponent ?? 6;
}
// test client connection
+25
View File
@@ -0,0 +1,25 @@
/**
* See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L8-L12
*/
export interface ChainRegistryDemonUnit {
denom: string;
exponent: number;
aliases: string[];
}
/**
* See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L5-L28
*/
export interface ChainRegistryAsset {
description: string;
denom_units: ChainRegistryDemonUnit[];
base: string;
name: string;
display: string;
symbol: string;
logo_URIs: {
png: string;
svg: string;
};
coingecko_id: string;
}
+14 -7
View File
@@ -1,11 +1,13 @@
import React from "react";
import { isSecp256k1Pubkey, pubkeyToAddress, SinglePubkey } from "@cosmjs/amino";
import HashView from "./HashView";
import StackableContainer from "../layout/StackableContainer";
interface Props {
/** Addresses of the multisig members */
members: string[];
/** Pubkeys of the multisig members */
members: readonly SinglePubkey[];
addressPrefix: string;
threshold: string;
}
@@ -19,11 +21,16 @@ const MultisigMembers = (props: Props) => (
<div>
<h2>Members</h2>
<ul>
{props.members.map((address: string) => (
<li key={address} className="info">
<HashView hash={address} />
</li>
))}
{props.members.map((pubkey) => {
const address = pubkeyToAddress(pubkey, props.addressPrefix);
// simplePubkey is base64 encoded compressed secp256k1 in almost every case. The fallback is added to be safe though.
const simplePubkey = isSecp256k1Pubkey(pubkey) ? pubkey.value : `${pubkey.type} pubkey`;
return (
<li key={address} className="info">
<HashView hash={`${address} (${simplePubkey})`} />
</li>
);
})}
</ul>
</div>
</div>
+3 -5
View File
@@ -1,11 +1,9 @@
import React from "react";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { DbTransaction } from "../../types";
import { useAppContext } from "../../context/AppContext";
import HashView from "./HashView";
import { printableCoin, printableCoins } from "../../lib/displayHelpers";
import { DbTransaction } from "../../types";
import StackableContainer from "../layout/StackableContainer";
import { printableCoins, printableCoin } from "../../lib/displayHelpers";
import HashView from "./HashView";
interface Props {
tx: DbTransaction;
+157
View File
@@ -0,0 +1,157 @@
import { Decimal } from "@cosmjs/math";
import { Account, calculateFee } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import axios from "axios";
import { NextRouter, withRouter } from "next/router";
import React, { useState } from "react";
import { useAppContext } from "../../context/AppContext";
import { checkAddress, exampleValidatorAddress } from "../../lib/displayHelpers";
import Button from "../inputs/Button";
import Input from "../inputs/Input";
import StackableContainer from "../layout/StackableContainer";
interface Props {
address: string | null;
accountOnChain: Account | null;
router: NextRouter;
closeForm: () => void;
}
const 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, gasLimit: number) => {
assert(Number.isSafeInteger(gasLimit) && gasLimit > 0, "gas limit must be a positive integer");
const amountInAtomics = Decimal.fromUserInput(
txAmount,
Number(state.chain.displayDenomExponent),
).atomics;
const 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(gasLimit, gasPrice);
const { accountOnChain } = props;
assert(accountOnChain, "accountOnChain missing");
return {
accountNumber: accountOnChain.accountNumber,
sequence: accountOnChain.sequence,
chainId: state.chain.chainId,
msgs: [msg],
fee: fee,
memo: memo,
};
};
const handleCreate = async () => {
assert(state.chain.addressPrefix, "addressPrefix missing");
const validatorAddressError = checkAddress(validatorAddress, state.chain.addressPrefix);
if (validatorAddressError) {
setAddressError(
`Invalid address for network ${state.chain.chainId}: ${validatorAddressError}`,
);
return;
}
setProcessing(true);
const tx = createTransaction(validatorAddress, 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. ${exampleValidatorAddress(0, state.chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label={`Amount (${state.chain.displayDenom})`}
name="amount"
type="number"
value={amount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAmount(e.target.value)}
/>
</div>
<div className="form-item">
<Input
label="Gas Limit"
name="gas"
type="number"
value={gas}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setGas(parseInt(e.target.value, 10))
}
/>
</div>
<div className="form-item">
<Input label="Gas Price" name="gas_price" type="string" value={gasPrice} disabled={true} />
</div>
<div className="form-item">
<Input
label="Memo"
name="memo"
type="text"
value={memo}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setMemo(e.target.value)}
/>
</div>
<Button label="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);
+4 -2
View File
@@ -28,7 +28,9 @@ const TransactionForm = (props: Props) => {
const [_processing, setProcessing] = useState(false);
const [addressError, setAddressError] = useState("");
const createTransaction = (txToAddress: string, txAmount: string, txGas: number) => {
const createTransaction = (txToAddress: string, txAmount: string, gasLimit: number) => {
assert(Number.isSafeInteger(gasLimit) && gasLimit > 0, "gas limit must be a positive integer");
const amountInAtomics = Decimal.fromUserInput(
txAmount,
Number(state.chain.displayDenomExponent),
@@ -48,7 +50,7 @@ const TransactionForm = (props: Props) => {
value: msgSend,
};
assert(gasPrice, "gasPrice missing");
const fee = calculateFee(Number(txGas), gasPrice);
const fee = calculateFee(gasLimit, gasPrice);
const { accountOnChain } = props;
assert(accountOnChain, "accountOnChain missing");
return {
+3
View File
@@ -33,6 +33,9 @@ const TransactionSigning = (props: Props) => {
try {
assert(state.chain.chainId, "chainId missing");
await window.keplr.enable(state.chain.chainId);
window.keplr.defaultOptions = {
sign: { preferNoSetFee: true, preferNoSetMemo: true, disableBalanceCheck: true },
};
const tempWalletAccount = await window.keplr.getKey(state.chain.chainId);
console.log(tempWalletAccount);
const tempHasSigned = props.signatures.some(
+10 -7
View File
@@ -17,14 +17,17 @@ function getChainInfoFromUrl(): ChainInfo {
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") || "",
denom: decodeURIComponent(params.get("denom") || ""),
displayDenom: decodeURIComponent(params.get("displayDenom") || ""),
displayDenomExponent: parseInt(
decodeURIComponent(params.get("displayDenomExponent") || ""),
10,
),
gasPrice: decodeURIComponent(params.get("gasPrice") || ""),
chainId: decodeURIComponent(params.get("chainId") || ""),
chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""),
registryName: params.get("registryName") || "",
addressPrefix: params.get("addressPrefix") || "",
registryName: decodeURIComponent(params.get("registryName") || ""),
addressPrefix: decodeURIComponent(params.get("addressPrefix") || ""),
explorerLink: decodeURIComponent(params.get("explorerLink") || ""),
};
+27 -14
View File
@@ -1,7 +1,7 @@
import { fromBase64, fromBech32, toBase64, toBech32 } from "@cosmjs/encoding";
import { sha512 } from "@cosmjs/crypto";
import { Decimal } from "@cosmjs/math";
import { Coin } from "@cosmjs/amino";
import { sha512 } from "@cosmjs/crypto";
import { fromBase64, fromBech32, toBase64, toBech32 } from "@cosmjs/encoding";
import { Decimal } from "@cosmjs/math";
import { ChainInfo } from "../types";
/**
@@ -69,34 +69,46 @@ const printableCoins = (coins: Coin[], chainInfo: ChainInfo) => {
/**
* Generates an example address for the configured blockchain.
*
* `index` can be set to a small integer in order to get different addresses. Defaults to 0.
* `index` can be set to a small integer in order to get different addresses.
*/
const exampleAddress = (index: number, chainAddressPrefix: string) => {
const usedIndex = index || 0;
function exampleAddress(index: number, chainAddressPrefix: string): string {
let data = fromBech32("cosmos1vqpjljwsynsn58dugz0w8ut7kun7t8ls2qkmsq").data;
for (let i = 0; i < usedIndex; ++i) {
for (let i = 0; i < index; ++i) {
data = sha512(data).slice(0, data.length); // hash one time and trim to original length
}
return toBech32(chainAddressPrefix, data);
};
}
/**
* Generates an example address for the configured blockchain.
*
* `index` can be set to a small integer in order to get different addresses.
*/
function exampleValidatorAddress(index: number, chainAddressPrefix: string): string {
let data = fromBech32("cosmosvaloper10v6wvdenee8r9l6wlsphcgur2ltl8ztkfrvj9a").data;
for (let i = 0; i < index; ++i) {
data = sha512(data).slice(0, data.length); // hash one time and trim to original length
}
const validatorPrefix = chainAddressPrefix + "valoper";
return toBech32(validatorPrefix, data);
}
/**
* Generates an example pubkey (secp256k1, compressed).
*
* `index` can be set to a small integer in order to get different addresses. Defaults to 0.
* `index` can be set to a small integer in order to get different addresses.
*
* Note: the keys are not necessarily valid (as in points on the chain) and should only be used
* as dummy data.
*/
const examplePubkey = (index: number) => {
const usedIndex = index || 0;
function examplePubkey(index: number): string {
let data = fromBase64("Akd/qKMWdZXyiMnSu6aFLpQEGDO0ijyal9mXUIcVaPNX");
for (let i = 0; i < usedIndex; ++i) {
for (let i = 0; i < index; ++i) {
data = sha512(data).slice(0, data.length); // hash one time and trim to original length
data[0] = index % 2 ? 0x02 : 0x03; // pubkeys have to start with 0x02 or 0x03
}
return toBase64(data);
};
}
/**
* Returns an error message for invalid addresses.
@@ -115,7 +127,7 @@ const checkAddress = (input: string, chainAddressPrefix: string) => {
return error.toString();
}
if (prefix !== chainAddressPrefix) {
if (!prefix.startsWith(chainAddressPrefix)) {
return `Expected address prefix '${chainAddressPrefix}' but got '${prefix}'`;
}
@@ -166,6 +178,7 @@ export {
printableCoin,
printableCoins,
exampleAddress,
exampleValidatorAddress,
examplePubkey,
checkAddress,
explorerLinkTx,
+3 -1
View File
@@ -3,7 +3,9 @@ import { DbAccount, DbSignature, DbTransaction } from "../types";
// Graphql base request for Faunadb
const graphqlReq = axios.create({
baseURL: "https://graphql.fauna.com/graphql",
// The fallback URL works for classic databases. See https://docs.fauna.com/fauna/current/learn/understanding/region_groups
// for more information about regions.
baseURL: process.env.FAUNADB_URL || "https://graphql.fauna.com/graphql",
headers: {
Authorization: `Bearer ${process.env.FAUNADB_SECRET}`,
},
+4381 -2749
View File
File diff suppressed because it is too large Load Diff
+28 -31
View File
@@ -8,38 +8,35 @@
"lint:fix": "eslint --max-warnings 0 \"./**/*.{js,jsx,ts,tsx}\" --fix"
},
"dependencies": {
"@cosmjs/amino": "^0.28.4",
"@cosmjs/crypto": "^0.28.4",
"@cosmjs/encoding": "^0.28.4",
"@cosmjs/ledger-amino": "^0.28.4",
"@cosmjs/math": "^0.28.4",
"@cosmjs/proto-signing": "^0.28.4",
"@cosmjs/stargate": "^0.28.4",
"@cosmjs/utils": "^0.28.4",
"@keplr-wallet/types": "^0.9.0-alpha.4",
"@ledgerhq/hw-transport-webusb": "^6.24.1",
"axios": "^0.21.1",
"copy-to-clipboard": "^3.3.1",
"faunadb": "^4.1.1",
"next": "^12.1.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-select": "^5.2.2",
"uuid": "^8.3.0"
"@cosmjs/amino": "^0.30.1",
"@cosmjs/crypto": "^0.30.1",
"@cosmjs/encoding": "^0.30.1",
"@cosmjs/ledger-amino": "^0.30.1",
"@cosmjs/math": "^0.30.1",
"@cosmjs/proto-signing": "^0.30.1",
"@cosmjs/stargate": "^0.30.1",
"@cosmjs/utils": "^0.30.1",
"@keplr-wallet/types": "^0.11.56",
"@ledgerhq/hw-transport-webusb": "^6.27.13",
"axios": "^1.3.5",
"copy-to-clipboard": "^3.3.3",
"faunadb": "^4.8.0",
"next": "^13.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.7.2"
},
"devDependencies": {
"@types/node": "^17.0.35",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@types/react-select": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^5.27.1",
"@typescript-eslint/parser": "^5.27.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",
"typescript": "^4.7.2"
"@types/node": "^18.15.11",
"@types/react": "^18.0.34",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"cosmjs-types": "^0.7.2",
"eslint": "^8.38.0",
"eslint-config-next": "^13.3.0",
"eslint-config-prettier": "^8.8.0",
"prettier": "^2.8.7",
"typescript": "^5.0.4"
}
}
@@ -1,12 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getMultisig } from "../../../../../../lib/graphqlHelpers";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function multisigAddressApi(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "GET":
try {
const multisigAddress = req.query.multisigAddress.toString();
const chainId = req.query.chainId.toString();
const multisigAddress = req.query.multisigAddress?.toString() || "";
const chainId = req.query.chainId?.toString() || "";
console.log("Function `getMultisig` invoked", multisigAddress, chainId);
const getRes = await getMultisig(multisigAddress, chainId);
if (!getRes.data.data.getMultisig) {
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createMultisig } from "../../../../../lib/graphqlHelpers";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function multisigApi(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
@@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { updateTxHash } from "../../../../lib/graphqlHelpers";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function transactionIDApi(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
const transactionID = req.query.transactionID.toString();
const transactionID = req.query.transactionID?.toString() || "";
const { txHash } = req.body;
console.log("Function `updateTransaction` invoked", txHash);
const saveRes = await updateTxHash(transactionID, txHash);
@@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createSignature } from "../../../../lib/graphqlHelpers";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function transactionIDApi(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
const transactionID = req.query.transactionID.toString();
const transactionID = req.query.transactionID?.toString() || "";
const data = req.body;
console.log("Function `createSignature` invoked", data);
const saveRes = await createSignature(data, transactionID);
+2 -2
View File
@@ -1,7 +1,7 @@
import { createTransaction } from "../../../lib/graphqlHelpers";
import type { NextApiRequest, NextApiResponse } from "next";
import { createTransaction } from "../../../lib/graphqlHelpers";
export default async function (req: NextApiRequest, res: NextApiResponse) {
export default async function transactionApi(req: NextApiRequest, res: NextApiResponse) {
switch (req.method) {
case "POST":
try {
+99 -79
View File
@@ -1,36 +1,33 @@
import React, { useState, useEffect } from "react";
import { pubkeyToAddress, Pubkey, MultisigThresholdPubkey } from "@cosmjs/amino";
import { MultisigThresholdPubkey, SinglePubkey } from "@cosmjs/amino";
import { Account, StargateClient } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { useRouter } from "next/router";
import { useAppContext } from "../../../context/AppContext";
import Button from "../../../components/inputs/Button";
import { getMultisigAccount } from "../../../lib/multisigHelpers";
import { useCallback, useEffect, useState } from "react";
import HashView from "../../../components/dataViews/HashView";
import MultisigHoldings from "../../../components/dataViews/MultisigHoldings";
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 UnDelegationForm from "../../../components/forms/UnDelegationForm";
import DelegationForm from "../../../components/forms/DelegationForm";
import ReDelegationForm from "../../../components/forms/ReDelegationForm";
import RewardsForm from "../../../components/forms/RewardsForm";
import TransactionForm from "../../../components/forms/TransactionForm";
import UnDelegationForm from "../../../components/forms/UnDelegationForm";
import Button from "../../../components/inputs/Button";
import Page from "../../../components/layout/Page";
import StackableContainer from "../../../components/layout/StackableContainer";
import { useAppContext } from "../../../context/AppContext";
import { getMultisigAccount } from "../../../lib/multisigHelpers";
function participantPubkeysFromMultisig(multisigPubkey: Pubkey) {
return multisigPubkey.value.pubkeys;
function participantPubkeysFromMultisig(
multisig: MultisigThresholdPubkey,
): readonly SinglePubkey[] {
return multisig.value.pubkeys;
}
function participantAddressesFromMultisig(multisigPubkey: Pubkey, addressPrefix: string) {
return participantPubkeysFromMultisig(multisigPubkey).map((p: Pubkey) =>
pubkeyToAddress(p, addressPrefix),
);
}
const multipage = () => {
const Multipage = () => {
const { state } = useAppContext();
const [showSendTxForm, setShowSendTxForm] = useState(false);
const [showDelegateTxForm, setShowDelegateTxForm] = useState(false);
const [showUnDelegateTxForm, setShowUnDelegateTxForm] = useState(false);
const [showReDelegateTxForm, setShowReDelegateTxForm] = useState(false);
const [showRewardsTxForm, setShowRewardsTxForm] = useState(false);
@@ -41,31 +38,34 @@ const multipage = () => {
const [accountError, setAccountError] = useState(null);
const router = useRouter();
const fetchMultisig = useCallback(
async (address: string) => {
setAccountError(null);
try {
assert(state.chain.nodeAddress, "Node address missing");
const client = await StargateClient.connect(state.chain.nodeAddress);
assert(state.chain.denom, "denom missing");
const tempHoldings = await client.getBalance(address, state.chain.denom);
setHoldings(tempHoldings);
const result = await getMultisigAccount(address, client);
setPubkey(result[0]);
setAccountOnChain(result[1]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setAccountError(error.message);
console.log("Account error:", error);
}
},
[state.chain.denom, state.chain.nodeAddress],
);
useEffect(() => {
const address = router.query.address?.toString();
if (address) {
setMultisigAddress(address);
fetchMultisig(address);
}
}, [state, router.query.address]);
const fetchMultisig = async (address: string) => {
setAccountError(null);
try {
assert(state.chain.nodeAddress, "Node address missing");
const client = await StargateClient.connect(state.chain.nodeAddress);
assert(state.chain.denom, "denom missing");
const tempHoldings = await client.getBalance(address, state.chain.denom);
setHoldings(tempHoldings);
const result = await getMultisigAccount(address, client);
setPubkey(result[0]);
setAccountOnChain(result[1]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setAccountError(error.message);
console.log("Account error:", error);
}
};
}, [fetchMultisig, router.query.address]);
assert(state.chain.addressPrefix, "address prefix missing");
@@ -84,7 +84,8 @@ const multipage = () => {
</StackableContainer>
{pubkey && (
<MultisigMembers
members={participantAddressesFromMultisig(pubkey, state.chain.addressPrefix)}
members={participantPubkeysFromMultisig(pubkey)}
addressPrefix={state.chain.addressPrefix}
threshold={pubkey.value.threshold}
/>
)}
@@ -112,6 +113,15 @@ const multipage = () => {
}}
/>
)}
{showDelegateTxForm && (
<DelegationForm
address={multisigAddress}
accountOnChain={accountOnChain}
closeForm={() => {
setShowDelegateTxForm(false);
}}
/>
)}
{showUnDelegateTxForm && (
<UnDelegationForm
address={multisigAddress}
@@ -139,46 +149,56 @@ const multipage = () => {
}}
/>
)}
{!showSendTxForm && !showUnDelegateTxForm && !showRewardsTxForm && !showReDelegateTxForm && (
<div className="interfaces">
<div className="col-1">
<MultisigHoldings holdings={holdings} />
{!showSendTxForm &&
!showDelegateTxForm &&
!showUnDelegateTxForm &&
!showRewardsTxForm &&
!showReDelegateTxForm && (
<div className="interfaces">
<div className="col-1">
<MultisigHoldings holdings={holdings} />
</div>
<div className="col-2">
<StackableContainer lessPadding>
<h2>New transaction</h2>
<p>
Once a transaction is created, it can be signed by the multisig members, and
then broadcast.
</p>
<Button
label="Create Transaction"
onClick={() => {
setShowSendTxForm(true);
}}
/>
<Button
label="Create Delegation"
onClick={() => {
setShowDelegateTxForm(true);
}}
/>
<Button
label="Create UnDelegation"
onClick={() => {
setShowUnDelegateTxForm(true);
}}
/>
<Button
label="Create Redelegate"
onClick={() => {
setShowReDelegateTxForm(true);
}}
/>
<Button
label="Claim Rewards"
onClick={() => {
setShowRewardsTxForm(true);
}}
/>
</StackableContainer>
</div>
</div>
<div className="col-2">
<StackableContainer lessPadding>
<h2>New transaction</h2>
<p>
Once a transaction is created, it can be signed by the multisig members, and then
broadcast.
</p>
<Button
label="Create Transaction"
onClick={() => {
setShowSendTxForm(true);
}}
/>
<Button
label="Create UnDelegation"
onClick={() => {
setShowUnDelegateTxForm(true);
}}
/>
<Button
label="Claim Rewards"
onClick={() => {
setShowRewardsTxForm(true);
}}
/>
<Button
label="Create Redelegate"
onClick={() => {
setShowReDelegateTxForm(true);
}}
/>
</StackableContainer>
</div>
</div>
)}
)}
</StackableContainer>
<style jsx>{`
.interfaces {
@@ -217,4 +237,4 @@ const multipage = () => {
);
};
export default multipage;
export default Multipage;
@@ -1,25 +1,22 @@
import axios from "axios";
import React from "react";
import { GetServerSideProps } from "next";
import { StargateClient, makeMultisignedTx, Account } from "@cosmjs/stargate";
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";
import { getMultisigAccount } from "../../../../lib/multisigHelpers";
import Page from "../../../../components/layout/Page";
import StackableContainer from "../../../../components/layout/StackableContainer";
import { fromBase64 } from "@cosmjs/encoding";
import { Account, StargateClient, makeMultisignedTxBytes } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import axios from "axios";
import { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import CompletedTransaction from "../../../../components/dataViews/CompletedTransaction";
import ThresholdInfo from "../../../../components/dataViews/ThresholdInfo";
import TransactionInfo from "../../../../components/dataViews/TransactionInfo";
import TransactionSigning from "../../../../components/forms/TransactionSigning";
import CompletedTransaction from "../../../../components/dataViews/CompletedTransaction";
import { assert } from "@cosmjs/utils";
import Button from "../../../../components/inputs/Button";
import Page from "../../../../components/layout/Page";
import StackableContainer from "../../../../components/layout/StackableContainer";
import { useAppContext } from "../../../../context/AppContext";
import { findTransactionByID } from "../../../../lib/graphqlHelpers";
import { getMultisigAccount } from "../../../../lib/multisigHelpers";
import { DbSignature, DbTransaction } from "../../../../types";
interface Props {
props: {
@@ -57,7 +54,7 @@ export const getServerSideProps: GetServerSideProps = async (context): Promise<P
};
};
const transactionPage = ({
const TransactionPage = ({
multisigAddress,
transactionJSON,
transactionID,
@@ -85,26 +82,29 @@ const transactionPage = ({
setCurrentSignatures((prevState: DbSignature[]) => [...prevState, signature]);
};
const fetchMultisig = useCallback(
async (address: string) => {
try {
assert(state.chain.nodeAddress, "Node address missing");
const client = await StargateClient.connect(state.chain.nodeAddress);
const result = await getMultisigAccount(address, client);
setPubkey(result[0]);
setAccountOnChain(result[1]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setAccountError(error.toString());
console.log("Account error:", error);
}
},
[state.chain.nodeAddress],
);
useEffect(() => {
const address = router.query.address?.toString();
if (address) {
fetchMultisig(address);
}
}, [state, router.query.address]);
const fetchMultisig = async (address: string) => {
try {
assert(state.chain.nodeAddress, "Node address missing");
const client = await StargateClient.connect(state.chain.nodeAddress);
const result = await getMultisigAccount(address, client);
setPubkey(result[0]);
setAccountOnChain(result[1]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setAccountError(error.toString());
console.log("Account error:", error);
}
};
}, [fetchMultisig, router.query.address]);
const broadcastTx = async () => {
try {
@@ -114,7 +114,7 @@ const transactionPage = ({
assert(accountOnChain, "Account on chain value missing.");
assert(pubkey, "Pubkey not found on chain or in database");
const bodyBytes = fromBase64(currentSignatures[0].bodyBytes);
const signedTx = makeMultisignedTx(
const signedTxBytes = makeMultisignedTxBytes(
pubkey,
txInfo.sequence,
txInfo.fee,
@@ -123,9 +123,7 @@ const transactionPage = ({
);
assert(state.chain.nodeAddress, "Node address missing");
const broadcaster = await StargateClient.connect(state.chain.nodeAddress);
const result = await broadcaster.broadcastTx(
Uint8Array.from(TxRaw.encode(signedTx).finish()),
);
const result = await broadcaster.broadcastTx(signedTxBytes);
console.log(result);
const _res = await axios.post(`/api/transaction/${transactionID}`, {
txHash: result.transactionHash,
@@ -200,4 +198,4 @@ const transactionPage = ({
);
};
export default transactionPage;
export default TransactionPage;
+3
View File
@@ -4,6 +4,9 @@ import { EncodeObject } from "@cosmjs/proto-signing";
declare global {
interface Window {
keplr: {
defaultOptions: {
sign: { preferNoSetFee: boolean; preferNoSetMemo: boolean; disableBalanceCheck: boolean };
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
enable: (chainId: string) => any;
getKey: (chainId: string) => Promise<WalletAccount>;