Merge pull request #226 from cosmos/feat/better-apis

Add type safe endpoints with zod
This commit is contained in:
Abel Fernández
2024-06-27 12:50:17 +02:00
committed by GitHub
29 changed files with 1191 additions and 898 deletions
+69 -80
View File
@@ -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>
+4 -20
View File
@@ -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({
+2 -2
View File
@@ -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) => {
+6 -12
View File
@@ -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({
+7 -7
View File
@@ -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
View File
@@ -14,7 +14,7 @@ type Transaction {
id: ID!
txHash: String
creator: Multisig
dataJSON: String
dataJSON: String!
signatures: [Signature] @hasInverse(field: transaction)
}
+13
View File
@@ -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";
+200
View File
@@ -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}`,
);
};
+80
View File
@@ -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;
};
+53
View File
@@ -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;
};
+158
View File
@@ -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;
};
-31
View File
@@ -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
View File
@@ -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;
};
-384
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+182 -3
View File
@@ -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"
}
+2
View File
@@ -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;
}
+29 -20
View File
@@ -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;
}
+23 -21
View File
@@ -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;
}
+29 -26
View File
@@ -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;
}
+57 -69
View File
@@ -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
View File
@@ -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;
};