Merge pull request #226 from cosmos/feat/better-apis
Add type safe endpoints with zod
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { DbTransaction } from "@/graphql";
|
||||
import { getDbMultisigTxs, getDbNonce } from "@/lib/api";
|
||||
import { ellideMiddle } from "@/lib/displayHelpers";
|
||||
import { getConnectError } from "@/lib/errorHelpers";
|
||||
import { getKeplrKey, getKeplrVerifySignature, useKeplrReconnect } from "@/lib/keplr";
|
||||
import { requestJson } from "@/lib/request";
|
||||
import { msgTypeCountsFromJson } from "@/lib/txMsgHelpers";
|
||||
import { cn, toastError } from "@/lib/utils";
|
||||
import { DbNonce, DbTransaction } from "@/types/db";
|
||||
import { WalletInfo } from "@/types/signing";
|
||||
import { toBase64 } from "@cosmjs/encoding";
|
||||
import { StargateClient } from "@cosmjs/stargate";
|
||||
@@ -47,9 +47,7 @@ export default function ListMultisigTxs({
|
||||
throw new Error(`Account not found on chain for ${address}`);
|
||||
}
|
||||
|
||||
const { nonce }: DbNonce = await requestJson(
|
||||
`/api/chain/${chain.chainId}/nonce/${accountOnChain.address}`,
|
||||
);
|
||||
const nonce = await getDbNonce(accountOnChain.address, chain.chainId);
|
||||
|
||||
const signature = await getKeplrVerifySignature(accountOnChain.address, chain, nonce);
|
||||
return signature;
|
||||
@@ -61,14 +59,7 @@ export default function ListMultisigTxs({
|
||||
async (address: string) => {
|
||||
try {
|
||||
const signature = await getSignature(address);
|
||||
|
||||
const fetchedTransactions: readonly DbTransaction[] = await requestJson(
|
||||
`/api/transaction/list`,
|
||||
{
|
||||
body: { signature, chain, multisigAddress },
|
||||
},
|
||||
);
|
||||
|
||||
const fetchedTransactions = await getDbMultisigTxs(multisigAddress, chain, signature);
|
||||
setTransactions(fetchedTransactions);
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to fetch transactions:", e);
|
||||
@@ -136,81 +127,79 @@ export default function ListMultisigTxs({
|
||||
<p>Loading transactions</p>
|
||||
</div>
|
||||
) : null}
|
||||
{walletInfo && transactions ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="multisigs-type"
|
||||
checked={showBroadcasted}
|
||||
onCheckedChange={(checked) => {
|
||||
setShowBroadcasted(checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="multisigs-type">Also show broadcasted transactions</Label>
|
||||
</div>
|
||||
) : null}
|
||||
{!showBroadcasted && pendingTxs && !pendingTxs.length
|
||||
? "No pending transactions found"
|
||||
: null}
|
||||
{showBroadcasted && transactions && !transactions.length ? "No transactions found" : null}
|
||||
{transactions?.length || pendingTxs?.length ? (
|
||||
<>
|
||||
{transactions?.length !== pendingTxs?.length ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="multisigs-type"
|
||||
checked={showBroadcasted}
|
||||
onCheckedChange={(checked) => {
|
||||
setShowBroadcasted(checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="multisigs-type">Also show broadcasted transactions</Label>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-2">
|
||||
{((showBroadcasted ? transactions : pendingTxs) ?? []).map((tx) => {
|
||||
const msgTypeCounts = msgTypeCountsFromJson(tx.dataJSON);
|
||||
const hasSigned = Boolean(
|
||||
tx.signatures.find(({ address }) => address === walletInfo?.address),
|
||||
);
|
||||
<div className="flex flex-col gap-2">
|
||||
{((showBroadcasted ? transactions : pendingTxs) ?? []).map((tx) => {
|
||||
const msgTypeCounts = msgTypeCountsFromJson(tx.dataJSON);
|
||||
const hasSigned = Boolean(
|
||||
tx.signatures.find(({ address }) => address === walletInfo?.address),
|
||||
);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tx.id}
|
||||
href={`/${chain.registryName}/${multisigAddress}/transaction/${tx.id}`}
|
||||
className="flex items-center space-x-2 rounded-md border p-2 transition-colors hover:cursor-pointer hover:bg-muted/50"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge
|
||||
className={cn(
|
||||
"font-mono text-white",
|
||||
hasSigned && "bg-yellow-600 hover:bg-yellow-500",
|
||||
tx.txHash && "bg-green-600 hover:bg-green-500",
|
||||
)}
|
||||
>
|
||||
{tx.signatures.length}/{multisigThreshold}
|
||||
return (
|
||||
<Link
|
||||
key={tx.id}
|
||||
href={`/${chain.registryName}/${multisigAddress}/transaction/${tx.id}`}
|
||||
className="flex items-center space-x-2 rounded-md border p-2 transition-colors hover:cursor-pointer hover:bg-muted/50"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Badge
|
||||
className={cn(
|
||||
"font-mono text-white",
|
||||
hasSigned && "bg-yellow-600 hover:bg-yellow-500",
|
||||
tx.txHash && "bg-green-600 hover:bg-green-500",
|
||||
)}
|
||||
>
|
||||
{tx.signatures.length}/{multisigThreshold}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
<p>signatures: {tx.signatures.length}</p>
|
||||
<p>threshold: {multisigThreshold}</p>
|
||||
<p>{hasSigned ? "Signed by you" : "Not signed by you"}</p>
|
||||
<p>
|
||||
{tx.txHash
|
||||
? `Broadcasted with hash: ${ellideMiddle(tx.txHash, 12)}`
|
||||
: "Not broadcasted"}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex-1 flex-col space-x-0.5">
|
||||
{msgTypeCounts.map(({ msgType, count }) => (
|
||||
<Badge key={msgType} className="pointer-events-none">
|
||||
{msgType}
|
||||
{count > 1 ? ` (${count})` : ""}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
<p>signatures: {tx.signatures.length}</p>
|
||||
<p>threshold: {multisigThreshold}</p>
|
||||
<p>{hasSigned ? "Signed by you" : "Not signed by you"}</p>
|
||||
<p>
|
||||
{tx.txHash
|
||||
? `Broadcasted with hash: ${ellideMiddle(tx.txHash, 12)}`
|
||||
: "Not broadcasted"}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex-1 flex-col space-x-0.5">
|
||||
{msgTypeCounts.map(({ msgType, count }) => (
|
||||
<Badge key={msgType} className="pointer-events-none">
|
||||
{msgType}
|
||||
{count > 1 ? ` (${count})` : ""}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 flex-col space-x-0.5">
|
||||
<p className="font-mono text-sm text-muted-foreground">{tx.id}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<MoveRightIcon className="w-5" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
<div className="flex-1 flex-col space-x-0.5">
|
||||
<p className="font-mono text-sm text-muted-foreground">{tx.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<MoveRightIcon className="w-5" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useChains } from "@/context/ChainsContext";
|
||||
import { FetchedMultisigs, getDbNonce, getDbUserMultisigs } from "@/lib/api";
|
||||
import { getConnectError } from "@/lib/errorHelpers";
|
||||
import { MultisigFromQuery } from "@/lib/graphqlHelpers";
|
||||
import { getKeplrKey, getKeplrVerifySignature, useKeplrReconnect } from "@/lib/keplr";
|
||||
import { requestJson } from "@/lib/request";
|
||||
import { toastError } from "@/lib/utils";
|
||||
import { DbNonce } from "@/types/db";
|
||||
import { WalletInfo } from "@/types/signing";
|
||||
import { MultisigThresholdPubkey } from "@cosmjs/amino";
|
||||
import { toBase64 } from "@cosmjs/encoding";
|
||||
@@ -20,11 +18,6 @@ import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
type FetchedMultisigs = {
|
||||
readonly created: readonly MultisigFromQuery[];
|
||||
readonly belonged: readonly MultisigFromQuery[];
|
||||
};
|
||||
|
||||
export default function ListUserMultisigs() {
|
||||
const { chain } = useChains();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -41,9 +34,7 @@ export default function ListUserMultisigs() {
|
||||
throw new Error(`Account not found on chain for ${address}`);
|
||||
}
|
||||
|
||||
const { nonce }: DbNonce = await requestJson(
|
||||
`/api/chain/${chain.chainId}/nonce/${accountOnChain.address}`,
|
||||
);
|
||||
const nonce = await getDbNonce(accountOnChain.address, chain.chainId);
|
||||
|
||||
const signature = await getKeplrVerifySignature(accountOnChain.address, chain, nonce);
|
||||
return signature;
|
||||
@@ -55,15 +46,8 @@ export default function ListUserMultisigs() {
|
||||
async (address: string) => {
|
||||
try {
|
||||
const signature = await getSignature(address);
|
||||
|
||||
const newMultisigs: FetchedMultisigs = await requestJson(
|
||||
`/api/chain/${chain.chainId}/multisig/list`,
|
||||
{
|
||||
body: { signature, chain },
|
||||
},
|
||||
);
|
||||
|
||||
setMultisigs(newMultisigs);
|
||||
const fetchedMultisigs = await getDbUserMultisigs(signature, chain);
|
||||
setMultisigs(fetchedMultisigs);
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to fetch multisigs:", e);
|
||||
toastError({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DbSignature } from "@/types/db";
|
||||
import { DbSignatureObj } from "@/graphql";
|
||||
import { MultisigThresholdPubkey } from "@cosmjs/amino";
|
||||
import { useEffect, useState } from "react";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
import CopyAndPaste from "./CopyAndPaste";
|
||||
|
||||
interface Props {
|
||||
signatures: DbSignature[];
|
||||
signatures: DbSignatureObj[];
|
||||
pubkey: MultisigThresholdPubkey;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DbTransactionJsonObj } from "@/types/db";
|
||||
import { DbTransactionParsedDataJson } from "@/graphql";
|
||||
import { MsgTypeUrls } from "@/types/txMsg";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { useChains } from "../../../context/ChainsContext";
|
||||
@@ -65,7 +65,7 @@ const TxMsgDetails = ({ typeUrl, value: msgValue }: EncodeObject) => {
|
||||
};
|
||||
|
||||
interface TransactionInfoProps {
|
||||
readonly tx: DbTransactionJsonObj;
|
||||
readonly tx: DbTransactionParsedDataJson;
|
||||
}
|
||||
|
||||
const TransactionInfo = ({ tx }: TransactionInfoProps) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { loadValidators } from "@/context/ChainsContext/helpers";
|
||||
import { DbTransactionParsedDataJson } from "@/graphql";
|
||||
import { createDbTx } from "@/lib/api";
|
||||
import { toastError, toastSuccess } from "@/lib/utils";
|
||||
import { DbTransactionJsonObj } from "@/types/db";
|
||||
import { MsgTypeUrl, MsgTypeUrls } from "@/types/txMsg";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
@@ -9,7 +10,6 @@ import { NextRouter, withRouter } from "next/router";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useChains } from "../../../context/ChainsContext";
|
||||
import { requestJson } from "../../../lib/request";
|
||||
import { exportMsgToJson, gasOfTx } from "../../../lib/txMsgHelpers";
|
||||
import Button from "../../inputs/Button";
|
||||
import Input from "../../inputs/Input";
|
||||
@@ -82,7 +82,7 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
return;
|
||||
}
|
||||
|
||||
const tx: DbTransactionJsonObj = {
|
||||
const txData: DbTransactionParsedDataJson = {
|
||||
accountNumber: accountOnChain.accountNumber,
|
||||
sequence: accountOnChain.sequence,
|
||||
chainId: chain.chainId,
|
||||
@@ -91,15 +91,9 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
memo,
|
||||
};
|
||||
|
||||
const { transactionID } = await requestJson("/api/transaction", {
|
||||
body: {
|
||||
dataJSON: JSON.stringify(tx),
|
||||
creator: accountOnChain.address,
|
||||
chainId: chain.chainId,
|
||||
},
|
||||
});
|
||||
toastSuccess("Transaction created with ID", transactionID);
|
||||
router.push(`/${chain.registryName}/${senderAddress}/transaction/${transactionID}`);
|
||||
const txId = await createDbTx(accountOnChain.address, chain.chainId, txData);
|
||||
toastSuccess("Transaction created with ID", txId);
|
||||
router.push(`/${chain.registryName}/${senderAddress}/transaction/${txId}`);
|
||||
} catch (e) {
|
||||
console.error("Failed to create transaction:", e);
|
||||
toastError({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DbSignatureObj, DbSignatureObjDraft, DbTransactionParsedDataJson } from "@/graphql";
|
||||
import { createDbSignature } from "@/lib/api";
|
||||
import { getKeplrAminoSigner, getKeplrKey, useKeplrReconnect } from "@/lib/keplr";
|
||||
import { toastError, toastSuccess } from "@/lib/utils";
|
||||
import { DbSignature, DbTransactionJsonObj } from "@/types/db";
|
||||
import { LoadingStates, SigningStatus } from "@/types/signing";
|
||||
import { MultisigThresholdPubkey, makeCosmoshubPath } from "@cosmjs/amino";
|
||||
import { createWasmAminoConverters, wasmTypes } from "@cosmjs/cosmwasm-stargate";
|
||||
@@ -20,17 +21,16 @@ import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useChains } from "../../context/ChainsContext";
|
||||
import { getConnectError } from "../../lib/errorHelpers";
|
||||
import { requestJson } from "../../lib/request";
|
||||
import HashView from "../dataViews/HashView";
|
||||
import Button from "../inputs/Button";
|
||||
import StackableContainer from "../layout/StackableContainer";
|
||||
|
||||
interface TransactionSigningProps {
|
||||
readonly signatures: DbSignature[];
|
||||
readonly tx: DbTransactionJsonObj;
|
||||
readonly signatures: DbSignatureObj[];
|
||||
readonly tx: DbTransactionParsedDataJson;
|
||||
readonly pubkey: MultisigThresholdPubkey;
|
||||
readonly transactionID: string;
|
||||
readonly addSignature: (signature: DbSignature) => void;
|
||||
readonly addSignature: (signature: DbSignatureObj) => void;
|
||||
}
|
||||
|
||||
const TransactionSigning = (props: TransactionSigningProps) => {
|
||||
@@ -177,12 +177,12 @@ const TransactionSigning = (props: TransactionSigningProps) => {
|
||||
throw new Error("This account has already signed");
|
||||
}
|
||||
|
||||
const signature = {
|
||||
const signature: Omit<DbSignatureObjDraft, "transaction"> = {
|
||||
bodyBytes: bases64EncodedBodyBytes,
|
||||
signature: bases64EncodedSignature,
|
||||
address: signerAddress,
|
||||
};
|
||||
await requestJson(`/api/transaction/${props.transactionID}/signature`, { body: signature });
|
||||
await createDbSignature(props.transactionID, signature);
|
||||
toastSuccess("Transaction signed by", signerAddress);
|
||||
props.addSignature(signature);
|
||||
setSigning("signed");
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type Transaction {
|
||||
id: ID!
|
||||
txHash: String
|
||||
creator: Multisig
|
||||
dataJSON: String
|
||||
dataJSON: String!
|
||||
signatures: [Signature] @hasInverse(field: transaction)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { GraphQLClient } from "graphql-request";
|
||||
|
||||
export const gqlClient = new GraphQLClient(
|
||||
process.env.DGRAPH_URL || "",
|
||||
process.env.DGRAPH_SECRET
|
||||
? { headers: { authorization: process.env.DGRAPH_SECRET || "" } }
|
||||
: undefined,
|
||||
);
|
||||
|
||||
export * from "./multisig";
|
||||
export * from "./nonce";
|
||||
export * from "./signature";
|
||||
export * from "./transaction";
|
||||
@@ -0,0 +1,200 @@
|
||||
import { gql } from "graphql-request";
|
||||
import { z } from "zod";
|
||||
import { gqlClient } from ".";
|
||||
|
||||
export const DbMultisig = z.object({
|
||||
id: z.string(),
|
||||
chainId: z.string(),
|
||||
address: z.string(),
|
||||
creator: z.string().nullish(),
|
||||
pubkeyJSON: z.string(),
|
||||
});
|
||||
export type DbMultisig = Readonly<z.infer<typeof DbMultisig>>;
|
||||
|
||||
export type DbMultisigDraft = Omit<DbMultisig, "id"> & { readonly creator: string };
|
||||
|
||||
export const DbMultisigId = DbMultisig.pick({ id: true });
|
||||
export type DbMultisigId = Readonly<z.infer<typeof DbMultisigId>>;
|
||||
|
||||
export const getMultisig = async (
|
||||
chainId: string,
|
||||
multisigAddress: string,
|
||||
): Promise<DbMultisig | null> => {
|
||||
type Response = { readonly queryMultisig: readonly DbMultisig[] };
|
||||
type Variables = { readonly chainId: string; readonly multisigAddress: string };
|
||||
|
||||
const { queryMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
query GetMultisig($chainId: String!, $multisigAddress: String!) {
|
||||
queryMultisig(filter: { chainId: { eq: $chainId }, address: { eq: $multisigAddress } }) {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
creator
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, multisigAddress },
|
||||
);
|
||||
|
||||
if (!queryMultisig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const multisigWithCreator = queryMultisig.find(({ creator }) => !!creator);
|
||||
const fetchedMultisig = multisigWithCreator ?? queryMultisig[0];
|
||||
|
||||
DbMultisig.parse(fetchedMultisig);
|
||||
|
||||
return fetchedMultisig;
|
||||
};
|
||||
|
||||
const getUniqueMultisigsPreferWithCreator = (
|
||||
multisigs: readonly DbMultisig[],
|
||||
): readonly DbMultisig[] => {
|
||||
const uniqueMultisigs = new Map<string, DbMultisig>();
|
||||
|
||||
for (const multisig of multisigs) {
|
||||
if (multisig.creator) {
|
||||
uniqueMultisigs.set(multisig.address, multisig);
|
||||
} else if (!uniqueMultisigs.has(multisig.address)) {
|
||||
uniqueMultisigs.set(multisig.address, multisig);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueMultisigs.values());
|
||||
};
|
||||
|
||||
const DbMultisigs = z.array(DbMultisig);
|
||||
|
||||
export const getCreatedMultisigs = async (
|
||||
chainId: string,
|
||||
creatorAddress: string,
|
||||
): Promise<readonly DbMultisig[]> => {
|
||||
type Response = { readonly queryMultisig: readonly DbMultisig[] };
|
||||
type Variables = { readonly chainId: string; readonly creatorAddress: string };
|
||||
|
||||
const { queryMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
query GetCreatedMultisigs($chainId: String!, $creatorAddress: String!) {
|
||||
queryMultisig(filter: { chainId: { eq: $chainId }, creator: { eq: $creatorAddress } }) {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
creator
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, creatorAddress },
|
||||
);
|
||||
|
||||
const fetchedMultisigs = getUniqueMultisigsPreferWithCreator(queryMultisig);
|
||||
DbMultisigs.parse(fetchedMultisigs);
|
||||
|
||||
return fetchedMultisigs;
|
||||
};
|
||||
|
||||
export const getBelongedMultisigs = async (
|
||||
chainId: string,
|
||||
memberPubkey: string,
|
||||
): Promise<readonly DbMultisig[]> => {
|
||||
type Response = { readonly queryMultisig: readonly DbMultisig[] };
|
||||
type Variables = { readonly chainId: string; readonly memberPubkey: string };
|
||||
|
||||
const { queryMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
query GetBelongedMultisigs($chainId: String!, $memberPubkey: String!) {
|
||||
queryMultisig(
|
||||
filter: { chainId: { eq: $chainId }, pubkeyJSON: { alloftext: $memberPubkey } }
|
||||
) {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
creator
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, memberPubkey },
|
||||
);
|
||||
|
||||
const fetchedMultisigs = getUniqueMultisigsPreferWithCreator(queryMultisig);
|
||||
DbMultisigs.parse(fetchedMultisigs);
|
||||
|
||||
return fetchedMultisigs;
|
||||
};
|
||||
|
||||
const DbMultisigAddress = DbMultisig.pick({ address: true });
|
||||
type DbMultisigAddress = Readonly<z.infer<typeof DbMultisigAddress>>;
|
||||
|
||||
export const createMultisig = async (multisig: DbMultisigDraft) => {
|
||||
const dbMultisig = await getMultisig(multisig.chainId, multisig.address);
|
||||
|
||||
// Create only if not exists
|
||||
if (!dbMultisig) {
|
||||
type Response = { readonly addMultisig: { readonly multisig: readonly DbMultisigAddress[] } };
|
||||
type Variables = DbMultisigDraft;
|
||||
|
||||
const { addMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation CreateMultisig(
|
||||
$chainId: String!
|
||||
$address: String!
|
||||
$creator: String!
|
||||
$pubkeyJSON: String!
|
||||
) {
|
||||
addMultisig(
|
||||
input: {
|
||||
chainId: $chainId
|
||||
address: $address
|
||||
creator: $creator
|
||||
pubkeyJSON: $pubkeyJSON
|
||||
}
|
||||
) {
|
||||
multisig {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
multisig,
|
||||
);
|
||||
|
||||
const createdMultisig = addMultisig.multisig[0];
|
||||
DbMultisigAddress.parse(createdMultisig);
|
||||
|
||||
return createdMultisig.address;
|
||||
}
|
||||
|
||||
// If provided multisig has a creator and the one on the DB doesn't, update it
|
||||
if (multisig.creator && !dbMultisig.creator) {
|
||||
type Response = {
|
||||
readonly updateMultisig: { readonly multisig: readonly DbMultisigAddress[] };
|
||||
};
|
||||
type Variables = { readonly id: string; readonly creator: string };
|
||||
|
||||
const { updateMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation UpdateMultisig($id: ID!, $creator: String!) {
|
||||
updateMultisig(input: { filter: { id: { eq: $id } }, set: { creator: $creator } }) {
|
||||
multisig {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id: dbMultisig.id, creator: multisig.creator },
|
||||
);
|
||||
|
||||
const updatedMultisig = updateMultisig.multisig[0];
|
||||
DbMultisigAddress.parse(updatedMultisig);
|
||||
|
||||
return updatedMultisig.address;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Multisig already exists on ${multisig.chainId} with address ${multisig.address}`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { gql } from "graphql-request";
|
||||
import { z } from "zod";
|
||||
import { gqlClient } from ".";
|
||||
|
||||
const DbNonceObjNonce = z.object({ nonce: z.number() });
|
||||
type DbNonceObjNonce = Readonly<z.infer<typeof DbNonceObjNonce>>;
|
||||
|
||||
export const getNonce = async (chainId: string, address: string) => {
|
||||
type QueryResponse = { readonly queryNonce: readonly DbNonceObjNonce[] };
|
||||
type QueryVariables = { readonly chainId: string; readonly address: string };
|
||||
|
||||
const { queryNonce } = await gqlClient.request<QueryResponse, QueryVariables>(
|
||||
gql`
|
||||
query GetNonce($chainId: String!, $address: String!) {
|
||||
queryNonce(filter: { chainId: { eq: $chainId }, address: { eq: $address } }) {
|
||||
nonce
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, address },
|
||||
);
|
||||
|
||||
const dbNonceObj = queryNonce.length ? queryNonce[0] : null;
|
||||
|
||||
if (dbNonceObj) {
|
||||
DbNonceObjNonce.parse(dbNonceObj);
|
||||
return dbNonceObj.nonce;
|
||||
}
|
||||
|
||||
type AddResponse = { readonly addNonce: { readonly nonce: readonly DbNonceObjNonce[] } };
|
||||
type AddVariables = { readonly chainId: string; readonly address: string };
|
||||
|
||||
const { addNonce } = await gqlClient.request<AddResponse, AddVariables>(
|
||||
gql`
|
||||
mutation CreateNonce($chainId: String!, $address: String!) {
|
||||
addNonce(input: { chainId: $chainId, address: $address, nonce: 1 }) {
|
||||
nonce {
|
||||
nonce
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, address },
|
||||
);
|
||||
|
||||
const createdNonceObj = addNonce.nonce[0];
|
||||
DbNonceObjNonce.parse(createdNonceObj);
|
||||
|
||||
return createdNonceObj.nonce;
|
||||
};
|
||||
|
||||
export const incrementNonce = async (chainId: string, address: string) => {
|
||||
const dbNonce = await getNonce(chainId, address);
|
||||
|
||||
type Response = { readonly updateNonce: { readonly nonce: readonly DbNonceObjNonce[] } };
|
||||
type Variables = { readonly chainId: string; readonly address: string; readonly nonce: number };
|
||||
|
||||
const { updateNonce } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation IncrementNonce($chainId: String!, $address: String!, $nonce: Int!) {
|
||||
updateNonce(
|
||||
input: {
|
||||
filter: { chainId: { eq: $chainId }, address: { eq: $address } }
|
||||
set: { nonce: $nonce }
|
||||
}
|
||||
) {
|
||||
nonce {
|
||||
nonce
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ chainId, address, nonce: dbNonce + 1 },
|
||||
);
|
||||
|
||||
const updatedNonceObj = updateNonce.nonce[0];
|
||||
DbNonceObjNonce.parse(updatedNonceObj);
|
||||
|
||||
return updatedNonceObj.nonce;
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { gql } from "graphql-request";
|
||||
import { z } from "zod";
|
||||
import { DbTransactionId, gqlClient } from ".";
|
||||
|
||||
// Calling DbSignatureObj to avoid DbSignatureSignature for the field type
|
||||
export const DbSignatureObj = z.object({
|
||||
bodyBytes: z.string(),
|
||||
signature: z.string(),
|
||||
address: z.string(),
|
||||
});
|
||||
export type DbSignatureObj = Readonly<z.infer<typeof DbSignatureObj>>;
|
||||
|
||||
export type DbSignatureObjDraft = DbSignatureObj & { readonly transaction: DbTransactionId };
|
||||
|
||||
const DbSignatureObjSignature = DbSignatureObj.pick({ signature: true });
|
||||
type DbSignatureObjSignature = Readonly<z.infer<typeof DbSignatureObjSignature>>;
|
||||
|
||||
export const createSignature = async (signature: DbSignatureObjDraft) => {
|
||||
type Response = {
|
||||
readonly addSignature: { readonly signature: readonly { readonly signature: string }[] };
|
||||
};
|
||||
type Variables = Omit<DbSignatureObjDraft, "transaction"> & { readonly transactionId: string };
|
||||
|
||||
const { addSignature } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation CreateSignature(
|
||||
$transactionId: ID!
|
||||
$bodyBytes: String!
|
||||
$signature: String!
|
||||
$address: String!
|
||||
) {
|
||||
addSignature(
|
||||
input: {
|
||||
transaction: { id: $transactionId }
|
||||
bodyBytes: $bodyBytes
|
||||
signature: $signature
|
||||
address: $address
|
||||
}
|
||||
) {
|
||||
signature {
|
||||
signature
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ ...signature, transactionId: signature.transaction.id },
|
||||
);
|
||||
|
||||
const signatureObjSignature = addSignature.signature[0];
|
||||
DbSignatureObjSignature.parse(signatureObjSignature);
|
||||
|
||||
return signatureObjSignature.signature;
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { StdFee } from "@cosmjs/amino";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { gql } from "graphql-request";
|
||||
import { z } from "zod";
|
||||
import { DbMultisig, DbMultisigId, DbSignatureObj, gqlClient } from ".";
|
||||
|
||||
export const DbTransaction = z.object({
|
||||
id: z.string(),
|
||||
txHash: z.string().nullish(),
|
||||
creator: z.lazy(() => DbMultisig.nullish()),
|
||||
// When parsed with JSON.parse it's DbTransactionParsedDataJson
|
||||
dataJSON: z.string(),
|
||||
signatures: z.lazy(() => z.array(DbSignatureObj)),
|
||||
});
|
||||
export type DbTransaction = Readonly<z.infer<typeof DbTransaction>>;
|
||||
|
||||
export interface DbTransactionParsedDataJson {
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
readonly chainId: string;
|
||||
readonly msgs: EncodeObject[];
|
||||
readonly fee: StdFee;
|
||||
readonly memo: string;
|
||||
}
|
||||
|
||||
export type DbTransactionDraft = Pick<DbTransaction, "dataJSON"> & { creator: DbMultisigId };
|
||||
|
||||
export const DbTransactionId = DbTransaction.pick({ id: true });
|
||||
export type DbTransactionId = Readonly<z.infer<typeof DbTransactionId>>;
|
||||
|
||||
export const getTransaction = async (id: string): Promise<DbTransaction | null> => {
|
||||
type Response = { readonly getTransaction: DbTransaction | null };
|
||||
type Variables = { readonly id: string };
|
||||
|
||||
const { getTransaction: fetchedTx } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
query GetTransaction($id: ID!) {
|
||||
getTransaction(id: $id) {
|
||||
id
|
||||
txHash
|
||||
creator {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
creator
|
||||
pubkeyJSON
|
||||
}
|
||||
dataJSON
|
||||
signatures {
|
||||
bodyBytes
|
||||
signature
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id },
|
||||
);
|
||||
|
||||
if (!fetchedTx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DbTransaction.parse(fetchedTx);
|
||||
|
||||
return fetchedTx;
|
||||
};
|
||||
|
||||
const DbMultisigTxs = z.object({ transactions: z.array(DbTransaction) });
|
||||
type DbMultisigTxs = Readonly<z.infer<typeof DbMultisigTxs>>;
|
||||
|
||||
export const getTransactions = async (creatorId: string): Promise<readonly DbTransaction[]> => {
|
||||
type Response = { readonly getMultisig: DbMultisigTxs };
|
||||
type Variables = { readonly creatorId: string };
|
||||
|
||||
const { getMultisig } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
query GetTransactions($creatorId: ID!) {
|
||||
getMultisig(id: $creatorId) {
|
||||
transactions {
|
||||
id
|
||||
txHash
|
||||
creator {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
creator
|
||||
pubkeyJSON
|
||||
}
|
||||
dataJSON
|
||||
signatures {
|
||||
bodyBytes
|
||||
signature
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ creatorId },
|
||||
);
|
||||
|
||||
const fetchedTxs: DbMultisigTxs = { transactions: getMultisig.transactions.reverse() };
|
||||
DbMultisigTxs.parse(fetchedTxs);
|
||||
|
||||
return fetchedTxs.transactions;
|
||||
};
|
||||
|
||||
export const createTransaction = async (transaction: DbTransactionDraft) => {
|
||||
type Response = { readonly addTransaction: { readonly transaction: readonly DbTransactionId[] } };
|
||||
type Variables = { readonly dataJSON: string; readonly creatorId: string };
|
||||
|
||||
const { addTransaction } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation CreateTransaction($dataJSON: String!, $creatorId: ID!) {
|
||||
addTransaction(input: { dataJSON: $dataJSON, creator: { id: $creatorId } }) {
|
||||
transaction {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ ...transaction, creatorId: transaction.creator.id },
|
||||
);
|
||||
|
||||
const createdTx = addTransaction.transaction[0];
|
||||
DbTransactionId.parse(createdTx);
|
||||
|
||||
return createdTx.id;
|
||||
};
|
||||
|
||||
const DbTransactionTxHash = z.object({ txHash: z.string() });
|
||||
type DbTransactionTxHash = Readonly<z.infer<typeof DbTransactionTxHash>>;
|
||||
|
||||
export const updateTxHash = async (id: string, txHash: string) => {
|
||||
type Response = {
|
||||
readonly updateTransaction: { readonly transaction: readonly DbTransactionTxHash[] };
|
||||
};
|
||||
type Variables = { readonly id: string; readonly txHash: string };
|
||||
|
||||
const { updateTransaction } = await gqlClient.request<Response, Variables>(
|
||||
gql`
|
||||
mutation UpdateTxHash($id: [ID!], $txHash: String!) {
|
||||
updateTransaction(input: { filter: { id: $id }, set: { txHash: $txHash } }) {
|
||||
transaction {
|
||||
txHash
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ id, txHash },
|
||||
);
|
||||
|
||||
const updatedTx = updateTransaction.transaction[0];
|
||||
DbTransactionTxHash.parse(updatedTx);
|
||||
|
||||
return updatedTx.txHash;
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { RequestConfig, requestJson } from "../lib/request";
|
||||
|
||||
type Status =
|
||||
| { readonly stage: "idle" | "loading"; readonly data?: never; readonly error?: never }
|
||||
| { readonly stage: "resolved"; readonly data: unknown; readonly error?: never }
|
||||
| { readonly stage: "rejected"; readonly data?: never; readonly error: string };
|
||||
|
||||
export default function useRequestJson(endpoint: string, config: RequestConfig = {}) {
|
||||
const [status, setStatus] = useState<Status>({ stage: "idle" });
|
||||
const { stage, data, error } = status;
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (stage === "idle") {
|
||||
setStatus({ stage: "loading" });
|
||||
}
|
||||
|
||||
if (stage === "loading") {
|
||||
try {
|
||||
const newData = await requestJson(endpoint, config);
|
||||
setStatus({ stage: "resolved", data: newData });
|
||||
} catch (e) {
|
||||
setStatus({ stage: "rejected", error: e instanceof Error ? e.message : String(e) });
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [config, endpoint, stage]);
|
||||
|
||||
return { loading: stage === "idle" || stage === "loading", data, error };
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import {
|
||||
DbMultisig,
|
||||
DbMultisigDraft,
|
||||
DbSignatureObjDraft,
|
||||
DbTransaction,
|
||||
DbTransactionParsedDataJson,
|
||||
} from "@/graphql";
|
||||
import { StdSignature } from "@cosmjs/amino";
|
||||
import { requestJson } from "./request";
|
||||
|
||||
export const getDbMultisig = async (multisigAddress: string, chainId: string) => {
|
||||
const multisig: DbMultisig = await requestJson(
|
||||
`/api/chain/${chainId}/multisig/${multisigAddress}`,
|
||||
);
|
||||
|
||||
return multisig;
|
||||
};
|
||||
|
||||
export type GetDbUserMultisigsBody = {
|
||||
readonly signature: StdSignature;
|
||||
readonly chain: ChainInfo;
|
||||
};
|
||||
export type FetchedMultisigs = {
|
||||
readonly created: readonly DbMultisig[];
|
||||
readonly belonged: readonly DbMultisig[];
|
||||
};
|
||||
export const getDbUserMultisigs = async (signature: StdSignature, chain: ChainInfo) => {
|
||||
const body: GetDbUserMultisigsBody = { signature, chain };
|
||||
|
||||
const multisigs: FetchedMultisigs = await requestJson(
|
||||
`/api/chain/${chain.chainId}/multisig/list`,
|
||||
{ body },
|
||||
);
|
||||
|
||||
return multisigs;
|
||||
};
|
||||
|
||||
export type CreateDbMultisigBody = DbMultisigDraft;
|
||||
export const createDbMultisig = async (multisig: DbMultisigDraft, chainId: string) => {
|
||||
const body: CreateDbMultisigBody = multisig;
|
||||
|
||||
const { dbMultisigAddress }: { dbMultisigAddress: string } = await requestJson(
|
||||
`/api/chain/${chainId}/multisig`,
|
||||
{ body },
|
||||
);
|
||||
|
||||
return dbMultisigAddress;
|
||||
};
|
||||
|
||||
export type GetDbMultisigTxsBody = {
|
||||
readonly signature: StdSignature;
|
||||
readonly chain: ChainInfo;
|
||||
readonly multisigAddress: string;
|
||||
};
|
||||
export const getDbMultisigTxs = async (
|
||||
multisigAddress: string,
|
||||
chain: ChainInfo,
|
||||
signature: StdSignature,
|
||||
) => {
|
||||
const body: GetDbMultisigTxsBody = { signature, chain, multisigAddress };
|
||||
const txs: readonly DbTransaction[] = await requestJson(`/api/transaction/list`, { body });
|
||||
|
||||
return txs;
|
||||
};
|
||||
|
||||
export type CreateDbTxBody = {
|
||||
readonly dataJSON: DbTransactionParsedDataJson;
|
||||
readonly creator: string;
|
||||
readonly chainId: string;
|
||||
};
|
||||
export const createDbTx = async (
|
||||
creatorAddress: string,
|
||||
chainId: string,
|
||||
dataJSON: DbTransactionParsedDataJson,
|
||||
) => {
|
||||
const body: CreateDbTxBody = { dataJSON, creator: creatorAddress, chainId };
|
||||
const { txId }: { txId: string } = await requestJson("/api/transaction", { body });
|
||||
|
||||
return txId;
|
||||
};
|
||||
|
||||
export type UpdateDbTxHashBody = {
|
||||
readonly txHash: string;
|
||||
};
|
||||
export const updateDbTxHash = async (txId: string, txHash: string) => {
|
||||
const body: UpdateDbTxHashBody = { txHash };
|
||||
|
||||
const { dbTxHash }: { dbTxHash: string } = await requestJson(`/api/transaction/${txId}`, {
|
||||
body,
|
||||
});
|
||||
|
||||
return dbTxHash;
|
||||
};
|
||||
|
||||
export type CreateDbSignatureBody = Omit<DbSignatureObjDraft, "transaction">;
|
||||
export const createDbSignature = async (
|
||||
txId: string,
|
||||
signatureObj: Omit<DbSignatureObjDraft, "transaction">,
|
||||
) => {
|
||||
const body: CreateDbSignatureBody = signatureObj;
|
||||
|
||||
const { signature }: { signature: string } = await requestJson(
|
||||
`/api/transaction/${txId}/signature`,
|
||||
{ body },
|
||||
);
|
||||
|
||||
return signature;
|
||||
};
|
||||
|
||||
export const getDbNonce = async (address: string, chainId: string) => {
|
||||
const { nonce }: { nonce: number } = await requestJson(`/api/chain/${chainId}/nonce/${address}`);
|
||||
return nonce;
|
||||
};
|
||||
@@ -1,384 +0,0 @@
|
||||
import { DbMultisig, DbNonce, DbSignature, DbTransaction, DbTransactionJsonObj } from "../types/db";
|
||||
import { requestGraphQlJson } from "./request";
|
||||
|
||||
/**
|
||||
* Creates multisig record in dgraph
|
||||
*
|
||||
* @param {object} multisig an object with address (string), pubkey JSON and chainId
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
const createMultisig = async (multisig: DbMultisig) => {
|
||||
return requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation AddMultisig {
|
||||
addMultisig(
|
||||
input: {
|
||||
chainId: "${multisig.chainId}"
|
||||
address: "${multisig.address}"
|
||||
creator: "${multisig.creator}"
|
||||
pubkeyJSON: ${JSON.stringify(multisig.pubkeyJSON)}
|
||||
}
|
||||
) {
|
||||
multisig {
|
||||
id
|
||||
chainId
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the format returned by the graphQL API.
|
||||
*
|
||||
* Keep the format in sync with `GetMultisigAccountResponse` because
|
||||
* we return the full object in the API. Right now address and chainId
|
||||
* are somewhat unnecessary to query but still nice for debgging.
|
||||
*/
|
||||
export interface MultisigFromQuery {
|
||||
address: string;
|
||||
chainId: string;
|
||||
pubkeyJSON: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets multisig pubkey from DB
|
||||
*
|
||||
* @param {string} address A multisig address.
|
||||
* @param {string} chainId The chainId the multisig belongs to.
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
async function getMultisig(address: string, chainId: string): Promise<MultisigFromQuery | null> {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query MultisigsByAddressAndChainId {
|
||||
queryMultisig(filter: {address: {eq: "${address}"}, chainId: {eq: "${chainId}"}}) {
|
||||
address
|
||||
chainId
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const elements: readonly MultisigFromQuery[] = result.data.queryMultisig;
|
||||
return elements.length ? elements[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets multisig id from DB
|
||||
*
|
||||
* @param {string} address A multisig address.
|
||||
* @param {string} chainId The chainId the multisig belongs to.
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
async function getMultisigId(address: string, chainId: string): Promise<string | null> {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query MultisigsByAddressAndChainId {
|
||||
queryMultisig(filter: {address: {eq: "${address}"}, chainId: {eq: "${chainId}"}}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const elements: readonly (MultisigFromQuery & { id: string })[] = result.data.queryMultisig;
|
||||
return elements.length ? elements[0].id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of multisigs from DB
|
||||
*
|
||||
* @param {string} chainId The chainId the multisig belongs to.
|
||||
* @param {string} creator The address of the creator of the multisig.
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
async function getCreatedMultisigs(chainId: string, creator: string) {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query MultisigsByChainIdAndCreator {
|
||||
queryMultisig(filter: {chainId: {eq: "${chainId}"}, creator: {eq: "${creator}"}}) {
|
||||
address
|
||||
chainId
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const elements: readonly MultisigFromQuery[] = result.data.queryMultisig.filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(el: MultisigFromQuery | null, i: number, arr: any) =>
|
||||
el !== null && i === arr.findIndex((el2: MultisigFromQuery) => el2.address === el.address),
|
||||
);
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets list of multisigs from DB
|
||||
*
|
||||
* @param {string} chainId The chainId the multisig belongs to.
|
||||
* @param {string} creator The address of a member of the multisig.
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
async function getBelongedMultisigs(chainId: string, memberPubkey: string) {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query MultisigsByChainIdAndCreator {
|
||||
queryMultisig(filter: {chainId: {eq: "${chainId}"}, pubkeyJSON: {alloftext: "${memberPubkey}"}}) {
|
||||
address
|
||||
chainId
|
||||
pubkeyJSON
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
const elements: readonly MultisigFromQuery[] = result.data.queryMultisig.filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(el: MultisigFromQuery | null, i: number, arr: any) =>
|
||||
el !== null && i === arr.findIndex((el2: MultisigFromQuery) => el2.address === el.address),
|
||||
);
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates transaction record in dgraph
|
||||
*
|
||||
* @param {object} transaction The base transaction
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
const createTransaction = async (transaction: DbTransactionJsonObj, creator: string) => {
|
||||
return requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation AddTransaction {
|
||||
addTransaction(input: { dataJSON: ${JSON.stringify(
|
||||
transaction,
|
||||
)}, creator: {id: "${creator}"}}) {
|
||||
transaction {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a transaction from dgraph
|
||||
*
|
||||
* @param {string} id dgraph resource id
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
const findTransactionByID = async (id: string) => {
|
||||
return requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query GetTransaction {
|
||||
getTransaction(id: "${id}") {
|
||||
dataJSON
|
||||
txHash
|
||||
signatures {
|
||||
address
|
||||
signature
|
||||
bodyBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getTransactions = async (creator: string): Promise<readonly DbTransaction[]> => {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query GetTransactionsByCreator {
|
||||
getMultisig(id: "${creator}") {
|
||||
transactions {
|
||||
id
|
||||
txHash
|
||||
dataJSON
|
||||
signatures {
|
||||
bodyBytes
|
||||
signature
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
return result.data.getMultisig.transactions.reverse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates txHash of transaction on dgraph
|
||||
*
|
||||
* @param {string} id dgraph resource id
|
||||
* @param {string} txHash tx hash returned from broadcasting a tx
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
const updateTxHash = async (id: string, txHash: string) => {
|
||||
return requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation UpdateTransaction {
|
||||
updateTransaction(
|
||||
input: { filter: { id: "${id}" }, set: { txHash: "${txHash}" } }
|
||||
) {
|
||||
transaction {
|
||||
id
|
||||
dataJSON
|
||||
txHash
|
||||
signatures {
|
||||
address
|
||||
signature
|
||||
bodyBytes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates signature record in dgraph
|
||||
*
|
||||
* @param {object} signature an object with bodyBytes (string) and signature set (Uint8 Array)
|
||||
* @param {string} transactionId id of the transaction to relate the signature with
|
||||
* @return Returns async function that makes a request to the dgraph graphql endpoint
|
||||
*/
|
||||
const createSignature = async (signature: DbSignature, transactionId: string) => {
|
||||
return requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation AddSignature {
|
||||
addSignature(
|
||||
input: {
|
||||
transaction: { id: "${transactionId}" }
|
||||
address: "${signature.address}"
|
||||
signature: "${signature.signature}"
|
||||
bodyBytes: "${signature.bodyBytes}"
|
||||
}
|
||||
) {
|
||||
signature {
|
||||
transaction {
|
||||
id
|
||||
}
|
||||
signature
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const nonceFromDbNonces = (dbNonces: readonly DbNonce[]): DbNonce | null => {
|
||||
const elements: readonly DbNonce[] = dbNonces.filter((el: DbNonce | null) => el !== null);
|
||||
const dbNonce = elements.length ? elements[0] : null;
|
||||
return dbNonce;
|
||||
};
|
||||
|
||||
async function createNonce(chainId: string, address: string): Promise<DbNonce | null> {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation AddNonce {
|
||||
addNonce(input: {chainId: "${chainId}", address: "${address}", nonce: 0}) {
|
||||
nonce {
|
||||
chainId
|
||||
address
|
||||
nonce
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
return nonceFromDbNonces(result.data.addNonce.nonce);
|
||||
}
|
||||
|
||||
async function getNonce(chainId: string, address: string): Promise<DbNonce | null> {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
query NonceByChainIdAndAddress {
|
||||
queryNonce(filter: {chainId: {eq: "${chainId}"}, address: {eq: "${address}"}}) {
|
||||
chainId
|
||||
address
|
||||
nonce
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log({ result });
|
||||
|
||||
return nonceFromDbNonces(result.data.queryNonce);
|
||||
}
|
||||
|
||||
async function updateNonce(
|
||||
chainId: string,
|
||||
address: string,
|
||||
newNonce: number,
|
||||
): Promise<DbNonce | null> {
|
||||
const result = await requestGraphQlJson({
|
||||
body: {
|
||||
query: `
|
||||
mutation UpdateNonce {
|
||||
updateNonce(input: {filter: {chainId: {eq: "${chainId}"}, address: {eq: "${address}"}}, set: {nonce: ${newNonce}}}) {
|
||||
nonce {
|
||||
chainId
|
||||
address
|
||||
nonce
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
||||
return nonceFromDbNonces(result.data.updateNonce.nonce);
|
||||
}
|
||||
|
||||
export {
|
||||
createMultisig,
|
||||
createNonce,
|
||||
createSignature,
|
||||
createTransaction,
|
||||
findTransactionByID,
|
||||
getBelongedMultisigs,
|
||||
getCreatedMultisigs,
|
||||
getMultisig,
|
||||
getMultisigId,
|
||||
getNonce,
|
||||
getTransactions,
|
||||
updateNonce,
|
||||
updateTxHash,
|
||||
};
|
||||
+7
-19
@@ -1,16 +1,13 @@
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import { DbMultisigDraft } from "@/graphql";
|
||||
import {
|
||||
createMultisigThresholdPubkey,
|
||||
MultisigThresholdPubkey,
|
||||
createMultisigThresholdPubkey,
|
||||
pubkeyToAddress,
|
||||
} from "@cosmjs/amino";
|
||||
import { Account, StargateClient } from "@cosmjs/stargate";
|
||||
import { createDbMultisig, getDbMultisig } from "./api";
|
||||
import { checkAddress, explorerLinkAccount } from "./displayHelpers";
|
||||
import { requestJson } from "./request";
|
||||
|
||||
export interface CreateMultisigAccountResponse {
|
||||
readonly address: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns array of compressed Secp256k1 pubkeys
|
||||
@@ -39,25 +36,18 @@ export const createMultisigFromCompressedSecp256k1Pubkeys = async (
|
||||
const multisigAddress = pubkeyToAddress(multisigPubkey, addressPrefix);
|
||||
|
||||
// save multisig to relational offchain database
|
||||
const multisig = {
|
||||
const multisig: DbMultisigDraft = {
|
||||
address: multisigAddress,
|
||||
pubkeyJSON: JSON.stringify(multisigPubkey),
|
||||
creator,
|
||||
chainId,
|
||||
};
|
||||
|
||||
const { address }: CreateMultisigAccountResponse = await requestJson(
|
||||
`/api/chain/${chainId}/multisig`,
|
||||
{ body: multisig },
|
||||
);
|
||||
const dbMultisigAddress = await createDbMultisig(multisig, chainId);
|
||||
|
||||
return address;
|
||||
return dbMultisigAddress;
|
||||
};
|
||||
|
||||
export interface GetMultisigAccountResponse {
|
||||
readonly pubkeyJSON: string;
|
||||
}
|
||||
|
||||
export type HostedMultisig =
|
||||
| {
|
||||
readonly hosted: "nowhere";
|
||||
@@ -92,9 +82,7 @@ export const getHostedMultisig = async (
|
||||
|
||||
hostedMultisig = await (async () => {
|
||||
try {
|
||||
const { pubkeyJSON }: GetMultisigAccountResponse = await requestJson(
|
||||
`/api/chain/${chainId}/multisig/${multisigAddress}`,
|
||||
);
|
||||
const { pubkeyJSON } = await getDbMultisig(multisigAddress, chainId);
|
||||
|
||||
const pubkeyOnDb = JSON.parse(pubkeyJSON);
|
||||
return { hosted: "db", pubkeyOnDb };
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { DbTransactionJsonObj } from "@/types/db";
|
||||
import { DbTransactionParsedDataJson } from "@/graphql";
|
||||
import { MsgCodecs, MsgTypeUrl, MsgTypeUrls } from "@/types/txMsg";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
|
||||
@@ -74,10 +74,10 @@ const importMsgFromJson = (msg: EncodeObject): EncodeObject => {
|
||||
throw new Error("Unknown msg type");
|
||||
};
|
||||
|
||||
export const dbTxFromJson = (txJson: string): DbTransactionJsonObj | null => {
|
||||
export const dbTxFromJson = (txJson: string): DbTransactionParsedDataJson | null => {
|
||||
try {
|
||||
const dbTx: DbTransactionJsonObj = JSON.parse(txJson);
|
||||
dbTx.msgs = dbTx.msgs.map(importMsgFromJson);
|
||||
const parsedDbTx: DbTransactionParsedDataJson = JSON.parse(txJson);
|
||||
const dbTx = { ...parsedDbTx, msgs: parsedDbTx.msgs.map(importMsgFromJson) };
|
||||
|
||||
return dbTx;
|
||||
} catch (error) {
|
||||
|
||||
Generated
+182
-3
@@ -64,6 +64,8 @@
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^7.1.0",
|
||||
"jest": "^29.6.4",
|
||||
"jest-environment-jsdom": "^29.6.4",
|
||||
"lucide-react": "^0.274.0",
|
||||
@@ -1374,6 +1376,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@graphql-typed-document-node/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
|
||||
"peerDependencies": {
|
||||
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.1.tgz",
|
||||
@@ -1967,6 +1977,93 @@
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@molt/command/-/command-0.9.0.tgz",
|
||||
"integrity": "sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==",
|
||||
"dependencies": {
|
||||
"@molt/types": "0.2.0",
|
||||
"alge": "0.8.1",
|
||||
"chalk": "^5.3.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"readline-sync": "^1.4.10",
|
||||
"string-length": "^6.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"type-fest": "^4.3.1",
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command/node_modules/ansi-regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command/node_modules/chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command/node_modules/string-length": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-6.0.0.tgz",
|
||||
"integrity": "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==",
|
||||
"dependencies": {
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command/node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/command/node_modules/type-fest": {
|
||||
"version": "4.20.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz",
|
||||
"integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@molt/types": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@molt/types/-/types-0.2.0.tgz",
|
||||
"integrity": "sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==",
|
||||
"dependencies": {
|
||||
"ts-toolbelt": "^9.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "13.4.19",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz",
|
||||
@@ -4630,6 +4727,17 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/alge": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/alge/-/alge-0.8.1.tgz",
|
||||
"integrity": "sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==",
|
||||
"dependencies": {
|
||||
"lodash.ismatch": "^4.4.0",
|
||||
"remeda": "^1.0.0",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"zod": "^3.17.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -7416,6 +7524,44 @@
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
|
||||
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-request": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.1.0.tgz",
|
||||
"integrity": "sha512-Ouu/lYVFhARS1aXeZoVJWnGT6grFJXTLwXJuK4mUGGRo0EUk1JkyYp43mdGmRgUVezpRm6V5Sq3t8jBDQcajng==",
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@molt/command": "^0.9.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"bin": {
|
||||
"graffle": "build/cli/generate.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dprint/formatter": "^0.3.0",
|
||||
"@dprint/typescript": "^0.91.1",
|
||||
"dprint": "^0.46.2",
|
||||
"graphql": "14 - 16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@dprint/formatter": {
|
||||
"optional": true
|
||||
},
|
||||
"@dprint/typescript": {
|
||||
"optional": true
|
||||
},
|
||||
"dprint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
@@ -9027,11 +9173,26 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||
},
|
||||
"node_modules/lodash.ismatch": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
|
||||
"integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
@@ -10375,6 +10536,14 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readline-sync": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readonly-date": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz",
|
||||
@@ -10478,6 +10647,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/remeda": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/remeda/-/remeda-1.61.0.tgz",
|
||||
"integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A=="
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -11345,6 +11519,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"node_modules/ts-toolbelt": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
|
||||
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w=="
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||
@@ -12005,9 +12184,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^7.1.0",
|
||||
"jest": "^29.6.4",
|
||||
"jest-environment-jsdom": "^29.6.4",
|
||||
"lucide-react": "^0.274.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isChainInfoFilled } from "@/context/ChainsContext/helpers";
|
||||
import { DbSignatureObj } from "@/graphql";
|
||||
import { getTransaction } from "@/graphql/transaction";
|
||||
import { updateDbTxHash } from "@/lib/api";
|
||||
import { toastError, toastSuccess } from "@/lib/utils";
|
||||
import { DbSignature } from "@/types/db";
|
||||
import { MultisigThresholdPubkey } from "@cosmjs/amino";
|
||||
import { fromBase64 } from "@cosmjs/encoding";
|
||||
import { Account, StargateClient, makeMultisignedTxBytes } from "@cosmjs/stargate";
|
||||
@@ -17,9 +19,7 @@ import Button from "../../../../components/inputs/Button";
|
||||
import Page from "../../../../components/layout/Page";
|
||||
import StackableContainer from "../../../../components/layout/StackableContainer";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { findTransactionByID } from "../../../../lib/graphqlHelpers";
|
||||
import { getHostedMultisig, isAccount } from "../../../../lib/multisigHelpers";
|
||||
import { requestJson } from "../../../../lib/request";
|
||||
import { dbTxFromJson } from "../../../../lib/txMsgHelpers";
|
||||
|
||||
interface Props {
|
||||
@@ -27,7 +27,7 @@ interface Props {
|
||||
transactionJSON: string;
|
||||
transactionID: string;
|
||||
txHash: string;
|
||||
signatures: DbSignature[];
|
||||
signatures: readonly DbSignatureObj[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,25 +35,19 @@ export const getServerSideProps: GetServerSideProps = async (context): Promise<P
|
||||
// get transaction info
|
||||
const transactionID = context.params?.transactionID?.toString();
|
||||
assert(transactionID, "Transaction ID missing");
|
||||
let transactionJSON;
|
||||
let txHash;
|
||||
let signatures;
|
||||
try {
|
||||
console.log("Function `findTransactionByID` invoked", transactionID);
|
||||
const getRes = await findTransactionByID(transactionID);
|
||||
console.log("success", getRes.data);
|
||||
txHash = getRes.data.getTransaction.txHash;
|
||||
transactionJSON = getRes.data.getTransaction.dataJSON;
|
||||
signatures = getRes.data.getTransaction.signatures;
|
||||
} catch (err: unknown) {
|
||||
console.log(err);
|
||||
console.log("Function `findTransactionByID` invoked", transactionID);
|
||||
const tx = await getTransaction(transactionID);
|
||||
if (!tx) {
|
||||
throw new Error("Transaction not found");
|
||||
}
|
||||
console.log("success", tx);
|
||||
|
||||
return {
|
||||
props: {
|
||||
transactionJSON,
|
||||
txHash,
|
||||
transactionJSON: tx.dataJSON,
|
||||
txHash: tx.txHash || "",
|
||||
transactionID,
|
||||
signatures,
|
||||
signatures: tx.signatures ?? [],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -66,7 +60,7 @@ const TransactionPage = ({
|
||||
}: {
|
||||
transactionJSON: string;
|
||||
transactionID: string;
|
||||
signatures: DbSignature[];
|
||||
signatures: DbSignatureObj[];
|
||||
txHash: string;
|
||||
}) => {
|
||||
const { chain } = useChains();
|
||||
@@ -79,8 +73,8 @@ const TransactionPage = ({
|
||||
const router = useRouter();
|
||||
const multisigAddress = router.query.address?.toString();
|
||||
|
||||
const addSignature = (signature: DbSignature) => {
|
||||
setCurrentSignatures((prevState: DbSignature[]) => [...prevState, signature]);
|
||||
const addSignature = (signature: DbSignatureObj) => {
|
||||
setCurrentSignatures((prevState: DbSignatureObj[]) => [...prevState, signature]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -133,10 +127,7 @@ const TransactionPage = ({
|
||||
|
||||
const broadcaster = await StargateClient.connect(chain.nodeAddress);
|
||||
const result = await broadcaster.broadcastTx(signedTxBytes);
|
||||
console.log(result);
|
||||
await requestJson(`/api/transaction/${transactionID}`, {
|
||||
body: { txHash: result.transactionHash },
|
||||
});
|
||||
await updateDbTxHash(transactionID, result.transactionHash);
|
||||
toastSuccess("Transaction broadcasted with hash", result.transactionHash);
|
||||
setTransactionHash(result.transactionHash);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
import { getMultisig } from "@/graphql/multisig";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getMultisig } from "../../../../../../lib/graphqlHelpers";
|
||||
|
||||
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() || "";
|
||||
console.log("Function `getMultisig` invoked", multisigAddress, chainId);
|
||||
const multisig = await getMultisig(multisigAddress, chainId);
|
||||
if (!multisig) {
|
||||
res.status(404).send("Multisig not found");
|
||||
return;
|
||||
}
|
||||
console.log("success", multisig);
|
||||
res.status(200).send(multisig);
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
const endpointErrMsg = "Failed to get multisig";
|
||||
|
||||
export default async function apiGetMultisig(req: NextApiRequest, res: NextApiResponse) {
|
||||
const chainId = req.query.chainId;
|
||||
const multisigAddress = req.query.multisigAddress;
|
||||
|
||||
if (
|
||||
req.method !== "GET" ||
|
||||
typeof chainId !== "string" ||
|
||||
!chainId ||
|
||||
typeof multisigAddress !== "string" ||
|
||||
!multisigAddress
|
||||
) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const multisig = await getMultisig(chainId, multisigAddress);
|
||||
if (!multisig) {
|
||||
throw new Error(`multisig not found with address ${multisigAddress} on chain ${chainId}`);
|
||||
}
|
||||
|
||||
res.status(200).send(multisig);
|
||||
console.log("Get multisig success", JSON.stringify(multisig, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import { createMultisig } from "@/lib/graphqlHelpers";
|
||||
import { createMultisig } from "@/graphql/multisig";
|
||||
import { CreateDbMultisigBody } from "@/lib/api";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function multisigApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
const data = req.body;
|
||||
console.log("Function `createMultisig` invoked", data);
|
||||
const saveRes = await createMultisig(data);
|
||||
console.log("success", saveRes);
|
||||
res.status(200).send(saveRes.data.addMultisig.multisig[0]);
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
const endpointErrMsg = "Failed to create multisig";
|
||||
|
||||
export default async function apiCreateMultisig(req: NextApiRequest, res: NextApiResponse) {
|
||||
const chainId = req.query.chainId;
|
||||
|
||||
if (req.method !== "POST" || typeof chainId !== "string" || !chainId) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const multisigDraft: CreateDbMultisigBody = req.body;
|
||||
|
||||
try {
|
||||
if (chainId !== multisigDraft.chainId) {
|
||||
throw new Error(
|
||||
`tried to create multisig on chain ${chainId} with data for chain ${multisigDraft.chainId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const dbMultisigAddress = await createMultisig(multisigDraft);
|
||||
res.status(200).send({ dbMultisigAddress });
|
||||
console.log("Create multisig success", JSON.stringify({ dbMultisigAddress }, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,68 +1,63 @@
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import {
|
||||
getBelongedMultisigs,
|
||||
getCreatedMultisigs,
|
||||
getNonce,
|
||||
updateNonce,
|
||||
} from "@/lib/graphqlHelpers";
|
||||
import { getBelongedMultisigs, getCreatedMultisigs } from "@/graphql/multisig";
|
||||
import { getNonce, incrementNonce } from "@/graphql/nonce";
|
||||
import { GetDbMultisigTxsBody } from "@/lib/api";
|
||||
import { verifyKeplrSignature } from "@/lib/keplr";
|
||||
import { decodeSignature, pubkeyToAddress } from "@cosmjs/amino";
|
||||
import { toBase64 } from "@cosmjs/encoding";
|
||||
import { StargateClient } from "@cosmjs/stargate";
|
||||
import { StdSignature } from "@keplr-wallet/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function multisigsApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
const { signature, chain }: { signature: StdSignature; chain: ChainInfo } = req.body;
|
||||
const endpointErrMsg = "Failed to list multisigs";
|
||||
|
||||
const chainId = typeof req.query.chainId === "string" ? req.query.chainId : "";
|
||||
if (chainId !== chain.chainId) {
|
||||
throw new Error(`Tried connecting to ${chainId} with data from ${chain.chainId}`);
|
||||
}
|
||||
export default async function apiListMultisigs(req: NextApiRequest, res: NextApiResponse) {
|
||||
const chainId = req.query.chainId;
|
||||
|
||||
const address = pubkeyToAddress(signature.pub_key, chain.addressPrefix);
|
||||
|
||||
const client = await StargateClient.connect(chain.nodeAddress);
|
||||
const accountOnChain = await client.getAccount(address);
|
||||
|
||||
if (!accountOnChain) {
|
||||
throw new Error("Account not found on chain for: " + address);
|
||||
}
|
||||
|
||||
const dbNonce = await getNonce(chainId, address);
|
||||
|
||||
if (!dbNonce) {
|
||||
throw new Error(`Nonce not found on ${chainId} for ${address}`);
|
||||
}
|
||||
|
||||
await updateNonce(chainId, address, dbNonce.nonce + 1);
|
||||
const verified = verifyKeplrSignature(signature, chain, dbNonce.nonce);
|
||||
|
||||
if (verified) {
|
||||
console.log("Function `getMultisigs` invoked", chainId, address);
|
||||
|
||||
const created = await getCreatedMultisigs(chainId, address);
|
||||
|
||||
const { pubkey: decodedPubKey } = decodeSignature(signature);
|
||||
const belonged = await getBelongedMultisigs(chainId, toBase64(decodedPubKey));
|
||||
|
||||
console.log("success", { created, belonged });
|
||||
res.status(200).send({ created, belonged });
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Signature failed for querying multisigs");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
if (req.method !== "POST" || typeof chainId !== "string" || !chainId) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const body: GetDbMultisigTxsBody = req.body;
|
||||
|
||||
try {
|
||||
if (chainId !== body.chain.chainId) {
|
||||
throw new Error(
|
||||
`tried listing multisigs from ${chainId} with data from ${body.chain.chainId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const address = pubkeyToAddress(body.signature.pub_key, body.chain.addressPrefix);
|
||||
|
||||
const client = await StargateClient.connect(body.chain.nodeAddress);
|
||||
const accountOnChain = await client.getAccount(address);
|
||||
|
||||
if (!accountOnChain) {
|
||||
throw new Error(`account with address ${address} not found on chain ${chainId}`);
|
||||
}
|
||||
|
||||
const dbNonce = await getNonce(chainId, address);
|
||||
const incrementedNonce = await incrementNonce(chainId, address);
|
||||
|
||||
if (incrementedNonce !== dbNonce + 1) {
|
||||
throw new Error("nonce increment failed");
|
||||
}
|
||||
|
||||
const verified = verifyKeplrSignature(body.signature, body.chain, dbNonce);
|
||||
|
||||
if (verified) {
|
||||
const created = await getCreatedMultisigs(chainId, address);
|
||||
const { pubkey: decodedPubKey } = decodeSignature(body.signature);
|
||||
const belonged = await getBelongedMultisigs(chainId, toBase64(decodedPubKey));
|
||||
|
||||
res.status(200).send({ created, belonged });
|
||||
console.log("List multisigs success", JSON.stringify({ created, belonged }, null, 2));
|
||||
} else {
|
||||
throw new Error("signature verification failed");
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
import { createNonce, getNonce } from "@/lib/graphqlHelpers";
|
||||
import { getNonce } from "@/graphql/nonce";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function nonceApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
try {
|
||||
const chainId = req.query.chainId?.toString() || "";
|
||||
const address = req.query.address?.toString() || "";
|
||||
const endpointErrMsg = "Failed to get nonce";
|
||||
|
||||
console.log("Function `getNonce` invoked", chainId, address);
|
||||
let nonce = await getNonce(chainId, address);
|
||||
if (nonce) {
|
||||
console.log("success", nonce);
|
||||
res.status(200).send(nonce);
|
||||
return;
|
||||
}
|
||||
export default async function apiGetNonce(req: NextApiRequest, res: NextApiResponse) {
|
||||
const chainId = req.query.chainId;
|
||||
const address = req.query.address;
|
||||
|
||||
nonce = await createNonce(chainId, address);
|
||||
if (!nonce) {
|
||||
throw new Error(`Nonce could not be created on ${chainId} for ${address}`);
|
||||
}
|
||||
if (
|
||||
req.method !== "GET" ||
|
||||
typeof chainId !== "string" ||
|
||||
!chainId ||
|
||||
typeof address !== "string" ||
|
||||
!address
|
||||
) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("success", nonce);
|
||||
res.status(200).send(nonce);
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const nonce = await getNonce(chainId, address);
|
||||
res.status(200).send({ nonce });
|
||||
console.log("Get nonce success", JSON.stringify({ nonce }, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { updateTxHash } from "@/graphql/transaction";
|
||||
import { UpdateDbTxHashBody } from "@/lib/api";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { updateTxHash } from "../../../../lib/graphqlHelpers";
|
||||
|
||||
export default async function transactionIDApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
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.updateTransaction.transaction[0]);
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
const endpointErrMsg = "Failed to update txHash";
|
||||
|
||||
export default async function apiUpdateTxHash(req: NextApiRequest, res: NextApiResponse) {
|
||||
const txId = req.query.transactionID;
|
||||
|
||||
if (req.method !== "POST" || typeof txId !== "string" || !txId) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const body: UpdateDbTxHashBody = req.body;
|
||||
|
||||
try {
|
||||
const dbTxHash = await updateTxHash(txId, body.txHash);
|
||||
res.status(200).send({ dbTxHash });
|
||||
console.log("Update txHash success", JSON.stringify({ dbTxHash }, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import { DbSignatureObjDraft, createSignature } from "@/graphql/signature";
|
||||
import { CreateDbSignatureBody } from "@/lib/api";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createSignature } from "../../../../lib/graphqlHelpers";
|
||||
|
||||
export default async function transactionIDApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
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.addSignature.signature[0]);
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
const endpointErrMsg = "Failed to create signature";
|
||||
|
||||
export default async function apiCreateSignature(req: NextApiRequest, res: NextApiResponse) {
|
||||
const txId = req.query.transactionID;
|
||||
|
||||
if (req.method !== "POST" || typeof txId !== "string" || !txId) {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const body: CreateDbSignatureBody = req.body;
|
||||
|
||||
try {
|
||||
const signatureObjDraft: DbSignatureObjDraft = { ...body, transaction: { id: txId } };
|
||||
const signature = await createSignature(signatureObjDraft);
|
||||
res.status(200).send({ signature });
|
||||
console.log("Create signature success", JSON.stringify({ signature }, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import { getMultisig } from "@/graphql/multisig";
|
||||
import { createTransaction } from "@/graphql/transaction";
|
||||
import { CreateDbTxBody } from "@/lib/api";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createTransaction, getMultisigId } from "../../../lib/graphqlHelpers";
|
||||
|
||||
export default async function transactionApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
const data = req.body;
|
||||
console.log("Function `createTransaction` invoked", data);
|
||||
const endpointErrMsg = "Failed to create transaction";
|
||||
|
||||
const multisigId = await getMultisigId(data.creator, data.chainId);
|
||||
if (!multisigId) {
|
||||
throw new Error("Multisig not found");
|
||||
}
|
||||
export default async function apiCreateTransaction(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const createTransactionResult = await createTransaction(data.dataJSON, multisigId);
|
||||
console.log("createTransactionResult:", createTransactionResult);
|
||||
res
|
||||
.status(200)
|
||||
.send({ transactionID: createTransactionResult.data.addTransaction.transaction[0].id });
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
const body: CreateDbTxBody = req.body;
|
||||
|
||||
try {
|
||||
const multisig = await getMultisig(body.chainId, body.creator);
|
||||
if (!multisig) {
|
||||
throw new Error(`multisig not found with address ${body.creator} on chain ${body.chainId}`);
|
||||
}
|
||||
|
||||
const txId = await createTransaction({
|
||||
dataJSON: JSON.stringify(body.dataJSON),
|
||||
creator: { id: multisig.id },
|
||||
});
|
||||
|
||||
res.status(200).send({ txId });
|
||||
console.log("Create transaction success", JSON.stringify({ txId }, null, 2));
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,78 +1,66 @@
|
||||
import { ChainInfo } from "@/context/ChainsContext/types";
|
||||
import {
|
||||
getMultisig,
|
||||
getMultisigId,
|
||||
getNonce,
|
||||
getTransactions,
|
||||
updateNonce,
|
||||
} from "@/lib/graphqlHelpers";
|
||||
import { getMultisig } from "@/graphql/multisig";
|
||||
import { getNonce, incrementNonce } from "@/graphql/nonce";
|
||||
import { getTransactions } from "@/graphql/transaction";
|
||||
import { GetDbMultisigTxsBody } from "@/lib/api";
|
||||
import { verifyKeplrSignature } from "@/lib/keplr";
|
||||
import { decodeSignature, pubkeyToAddress } from "@cosmjs/amino";
|
||||
import { toBase64 } from "@cosmjs/encoding";
|
||||
import { StargateClient } from "@cosmjs/stargate";
|
||||
import { StdSignature } from "@keplr-wallet/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function transactionsApi(req: NextApiRequest, res: NextApiResponse) {
|
||||
switch (req.method) {
|
||||
case "POST":
|
||||
try {
|
||||
const {
|
||||
signature,
|
||||
chain,
|
||||
multisigAddress,
|
||||
}: { signature: StdSignature; chain: ChainInfo; multisigAddress: string } = req.body;
|
||||
const endpointErrMsg = "Failed to list transactions";
|
||||
|
||||
const multisig = await getMultisig(multisigAddress, chain.chainId);
|
||||
if (!multisig) {
|
||||
throw new Error("Multisig not found");
|
||||
}
|
||||
|
||||
const { pubkey: decodedPubKey } = decodeSignature(signature);
|
||||
|
||||
if (!multisig.pubkeyJSON.includes(toBase64(decodedPubKey))) {
|
||||
throw new Error("You don't belong to the multisig");
|
||||
}
|
||||
|
||||
const address = pubkeyToAddress(signature.pub_key, chain.addressPrefix);
|
||||
|
||||
const client = await StargateClient.connect(chain.nodeAddress);
|
||||
const accountOnChain = await client.getAccount(address);
|
||||
|
||||
if (!accountOnChain) {
|
||||
throw new Error("Account not found on chain for: " + address);
|
||||
}
|
||||
|
||||
const dbNonce = await getNonce(chain.chainId, address);
|
||||
|
||||
if (!dbNonce) {
|
||||
throw new Error(`Nonce not found on ${chain.chainId} for ${address}`);
|
||||
}
|
||||
|
||||
await updateNonce(chain.chainId, address, dbNonce.nonce + 1);
|
||||
const verified = verifyKeplrSignature(signature, chain, dbNonce.nonce);
|
||||
|
||||
if (verified) {
|
||||
const multisigId = await getMultisigId(multisigAddress, chain.chainId);
|
||||
if (!multisigId) {
|
||||
throw new Error("Multisig not found");
|
||||
}
|
||||
console.log("Function `getTransactions` invoked", chain.chainId, address);
|
||||
const transactions = await getTransactions(multisigId);
|
||||
console.log("success", transactions);
|
||||
res.status(200).send(transactions);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Signature failed for querying multisigs");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
res.status(400).send(err.message);
|
||||
return;
|
||||
}
|
||||
export default async function apiListTransactions(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const body: GetDbMultisigTxsBody = req.body;
|
||||
|
||||
try {
|
||||
const multisig = await getMultisig(body.chain.chainId, body.multisigAddress);
|
||||
if (!multisig) {
|
||||
throw new Error(
|
||||
`multisig not found with address ${body.multisigAddress} on chain ${body.chain.chainId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { pubkey: decodedPubKey } = decodeSignature(body.signature);
|
||||
|
||||
if (!multisig.pubkeyJSON.includes(toBase64(decodedPubKey))) {
|
||||
throw new Error("your account does not belong to the multisig");
|
||||
}
|
||||
|
||||
const address = pubkeyToAddress(body.signature.pub_key, body.chain.addressPrefix);
|
||||
|
||||
const client = await StargateClient.connect(body.chain.nodeAddress);
|
||||
const accountOnChain = await client.getAccount(address);
|
||||
|
||||
if (!accountOnChain) {
|
||||
throw new Error(`account with address ${address} not found on chain ${body.chain.chainId}`);
|
||||
}
|
||||
|
||||
const dbNonce = await getNonce(body.chain.chainId, address);
|
||||
const incrementedNonce = await incrementNonce(body.chain.chainId, address);
|
||||
|
||||
if (incrementedNonce !== dbNonce + 1) {
|
||||
throw new Error("nonce increment failed");
|
||||
}
|
||||
|
||||
const verified = verifyKeplrSignature(body.signature, body.chain, dbNonce);
|
||||
|
||||
if (verified) {
|
||||
const transactions = await getTransactions(multisig.id);
|
||||
res.status(200).send(transactions);
|
||||
console.log("List transactions success", JSON.stringify(transactions, null, 2));
|
||||
} else {
|
||||
throw new Error("signature verification failed");
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
res
|
||||
.status(400)
|
||||
.send(err instanceof Error ? `${endpointErrMsg}: ${err.message}` : endpointErrMsg);
|
||||
}
|
||||
// no route matched
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { StdFee } from "@cosmjs/amino";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
|
||||
export interface DbSignature {
|
||||
bodyBytes: string;
|
||||
signature: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface DbTransaction {
|
||||
id: string;
|
||||
txHash: string;
|
||||
dataJSON: string;
|
||||
signatures: DbSignature[];
|
||||
}
|
||||
|
||||
export interface DbTransactionJsonObj {
|
||||
accountNumber: number;
|
||||
sequence: number;
|
||||
chainId: string;
|
||||
msgs: EncodeObject[];
|
||||
fee: StdFee;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
export interface DbMultisig {
|
||||
chainId: string;
|
||||
address: string;
|
||||
creator: string;
|
||||
pubkeyJSON: string;
|
||||
}
|
||||
|
||||
export type DbNonce = {
|
||||
readonly chainId: string;
|
||||
readonly address: string;
|
||||
readonly nonce: number;
|
||||
};
|
||||
Reference in New Issue
Block a user