From 8828f13eb0158d26ad6ba058fa7375267b4feced Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:31:21 +0200 Subject: [PATCH 01/15] Remove unused hook --- hooks/useRequestJson.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 hooks/useRequestJson.ts diff --git a/hooks/useRequestJson.ts b/hooks/useRequestJson.ts deleted file mode 100644 index b7c6d5a..0000000 --- a/hooks/useRequestJson.ts +++ /dev/null @@ -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({ 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 }; -} From e098561b9e93485eb82714deea00cf0fe211a162 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:31:33 +0200 Subject: [PATCH 02/15] Fix schema nullable --- db-schema.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-schema.graphql b/db-schema.graphql index 23d55b3..c362df6 100644 --- a/db-schema.graphql +++ b/db-schema.graphql @@ -14,7 +14,7 @@ type Transaction { id: ID! txHash: String creator: Multisig - dataJSON: String + dataJSON: String! signatures: [Signature] @hasInverse(field: transaction) } From 09d7c6abbe273a17ffec6c21fafdfe8ceab59aee Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:31:52 +0200 Subject: [PATCH 03/15] Add graphql dependencies --- package-lock.json | 185 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 + 2 files changed, 184 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc2c5c7..445bbe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index 3db288d..cf1f047 100644 --- a/package.json +++ b/package.json @@ -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", From 60d199009ef7ddb09c7a505700afc85c1e4db6b9 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:33:40 +0200 Subject: [PATCH 04/15] Add multisig graphql helpers --- graphql/multisig.ts | 200 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 graphql/multisig.ts diff --git a/graphql/multisig.ts b/graphql/multisig.ts new file mode 100644 index 0000000..aeb9735 --- /dev/null +++ b/graphql/multisig.ts @@ -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>; + +export type DbMultisigDraft = Omit & { readonly creator: string }; + +export const DbMultisigId = DbMultisig.pick({ id: true }); +export type DbMultisigId = Readonly>; + +export const getMultisig = async ( + chainId: string, + multisigAddress: string, +): Promise => { + type Response = { readonly queryMultisig: readonly DbMultisig[] }; + type Variables = { readonly chainId: string; readonly multisigAddress: string }; + + const { queryMultisig } = await gqlClient.request( + 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(); + + 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 => { + type Response = { readonly queryMultisig: readonly DbMultisig[] }; + type Variables = { readonly chainId: string; readonly creatorAddress: string }; + + const { queryMultisig } = await gqlClient.request( + 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 => { + type Response = { readonly queryMultisig: readonly DbMultisig[] }; + type Variables = { readonly chainId: string; readonly memberPubkey: string }; + + const { queryMultisig } = await gqlClient.request( + 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>; + +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( + 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( + 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}`, + ); +}; From 9ae437bbcd41c2968772a492ac2b03b000dea744 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:33:49 +0200 Subject: [PATCH 05/15] Add transaction graphql helpers --- graphql/transaction.ts | 158 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 graphql/transaction.ts diff --git a/graphql/transaction.ts b/graphql/transaction.ts new file mode 100644 index 0000000..1e904cf --- /dev/null +++ b/graphql/transaction.ts @@ -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>; + +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 & { creator: DbMultisigId }; + +export const DbTransactionId = DbTransaction.pick({ id: true }); +export type DbTransactionId = Readonly>; + +export const getTransaction = async (id: string): Promise => { + type Response = { readonly getTransaction: DbTransaction | null }; + type Variables = { readonly id: string }; + + const { getTransaction: fetchedTx } = await gqlClient.request( + 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>; + +export const getTransactions = async (creatorId: string): Promise => { + type Response = { readonly getMultisig: DbMultisigTxs }; + type Variables = { readonly creatorId: string }; + + const { getMultisig } = await gqlClient.request( + 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( + 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>; + +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( + 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; +}; From f359c3449af1fb2891b23ad685ba35ad30af457e Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:34:09 +0200 Subject: [PATCH 06/15] Add signature graphql helpers --- graphql/signature.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 graphql/signature.ts diff --git a/graphql/signature.ts b/graphql/signature.ts new file mode 100644 index 0000000..d4e9b9f --- /dev/null +++ b/graphql/signature.ts @@ -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>; + +export type DbSignatureObjDraft = DbSignatureObj & { readonly transaction: DbTransactionId }; + +const DbSignatureObjSignature = DbSignatureObj.pick({ signature: true }); +type DbSignatureObjSignature = Readonly>; + +export const createSignature = async (signature: DbSignatureObjDraft) => { + type Response = { + readonly addSignature: { readonly signature: readonly { readonly signature: string }[] }; + }; + type Variables = Omit & { readonly transactionId: string }; + + const { addSignature } = await gqlClient.request( + 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; +}; From 73616dc99181b89de0c3a241f0729bf7f51f5850 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:34:16 +0200 Subject: [PATCH 07/15] Add nonce graphql helpers --- graphql/nonce.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 graphql/nonce.ts diff --git a/graphql/nonce.ts b/graphql/nonce.ts new file mode 100644 index 0000000..8c32c04 --- /dev/null +++ b/graphql/nonce.ts @@ -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>; + +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( + 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( + 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( + 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; +}; From 66fab22501e6c51e3c2da517ad280bacc72f12a8 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:34:27 +0200 Subject: [PATCH 08/15] Add gqlClient and exports --- graphql/index.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 graphql/index.ts diff --git a/graphql/index.ts b/graphql/index.ts new file mode 100644 index 0000000..ae144b3 --- /dev/null +++ b/graphql/index.ts @@ -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"; From 7c61033120a9321dffa52c947fd473d14ce3fd80 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:34:36 +0200 Subject: [PATCH 09/15] Remove old types --- types/db.ts | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 types/db.ts diff --git a/types/db.ts b/types/db.ts deleted file mode 100644 index 64cfd39..0000000 --- a/types/db.ts +++ /dev/null @@ -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; -}; From b5d8abe629028fc8ab169b020ced891f8321fdc3 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:35:18 +0200 Subject: [PATCH 10/15] Remove old helpers --- lib/graphqlHelpers.ts | 384 ------------------------------------------ 1 file changed, 384 deletions(-) delete mode 100644 lib/graphqlHelpers.ts diff --git a/lib/graphqlHelpers.ts b/lib/graphqlHelpers.ts deleted file mode 100644 index cab2736..0000000 --- a/lib/graphqlHelpers.ts +++ /dev/null @@ -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 { - 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 { - 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 => { - 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 { - 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 { - 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 { - 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, -}; From 89b4b74917fcb92dfda8e89999ef1d16df97fa81 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:35:58 +0200 Subject: [PATCH 11/15] Add api endpoint helpers --- lib/api.ts | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 lib/api.ts diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..7c05928 --- /dev/null +++ b/lib/api.ts @@ -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; +export const createDbSignature = async ( + txId: string, + signatureObj: Omit, +) => { + 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; +}; From a4be5fe63ec0d8e8c6bbdd7c0f9a40b1f9e3d992 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:36:10 +0200 Subject: [PATCH 12/15] Refactor api endpoints --- .../multisig/[multisigAddress]/index.ts | 56 ++++---- pages/api/chain/[chainId]/multisig/index.ts | 49 ++++--- .../chain/[chainId]/multisig/list/index.ts | 111 ++++++++------- .../chain/[chainId]/nonce/[address]/index.tsx | 54 ++++---- .../api/transaction/[transactionID]/index.ts | 44 +++--- .../transaction/[transactionID]/signature.ts | 45 ++++--- pages/api/transaction/index.ts | 55 ++++---- pages/api/transaction/list/index.ts | 126 ++++++++---------- 8 files changed, 270 insertions(+), 270 deletions(-) diff --git a/pages/api/chain/[chainId]/multisig/[multisigAddress]/index.ts b/pages/api/chain/[chainId]/multisig/[multisigAddress]/index.ts index 0c782b7..a2d9d7a 100644 --- a/pages/api/chain/[chainId]/multisig/[multisigAddress]/index.ts +++ b/pages/api/chain/[chainId]/multisig/[multisigAddress]/index.ts @@ -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; } diff --git a/pages/api/chain/[chainId]/multisig/index.ts b/pages/api/chain/[chainId]/multisig/index.ts index a18f63f..73c25ac 100644 --- a/pages/api/chain/[chainId]/multisig/index.ts +++ b/pages/api/chain/[chainId]/multisig/index.ts @@ -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; } diff --git a/pages/api/chain/[chainId]/multisig/list/index.ts b/pages/api/chain/[chainId]/multisig/list/index.ts index b69e433..f2209c9 100644 --- a/pages/api/chain/[chainId]/multisig/list/index.ts +++ b/pages/api/chain/[chainId]/multisig/list/index.ts @@ -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; } diff --git a/pages/api/chain/[chainId]/nonce/[address]/index.tsx b/pages/api/chain/[chainId]/nonce/[address]/index.tsx index 56696e2..7f54a34 100644 --- a/pages/api/chain/[chainId]/nonce/[address]/index.tsx +++ b/pages/api/chain/[chainId]/nonce/[address]/index.tsx @@ -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; } diff --git a/pages/api/transaction/[transactionID]/index.ts b/pages/api/transaction/[transactionID]/index.ts index 44470d0..8c6d219 100644 --- a/pages/api/transaction/[transactionID]/index.ts +++ b/pages/api/transaction/[transactionID]/index.ts @@ -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; } diff --git a/pages/api/transaction/[transactionID]/signature.ts b/pages/api/transaction/[transactionID]/signature.ts index e5678db..93b8548 100644 --- a/pages/api/transaction/[transactionID]/signature.ts +++ b/pages/api/transaction/[transactionID]/signature.ts @@ -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; } diff --git a/pages/api/transaction/index.ts b/pages/api/transaction/index.ts index 4501082..048cb0c 100644 --- a/pages/api/transaction/index.ts +++ b/pages/api/transaction/index.ts @@ -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; } diff --git a/pages/api/transaction/list/index.ts b/pages/api/transaction/list/index.ts index f989101..6ad5f7c 100644 --- a/pages/api/transaction/list/index.ts +++ b/pages/api/transaction/list/index.ts @@ -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; } From 8befb2a0d8a0c544121133a47e59b81da75970ea Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:37:20 +0200 Subject: [PATCH 13/15] Adapt tx component --- .../[address]/transaction/[transactionID].tsx | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/pages/[chainName]/[address]/transaction/[transactionID].tsx b/pages/[chainName]/[address]/transaction/[transactionID].tsx index b391e43..51a098a 100644 --- a/pages/[chainName]/[address]/transaction/[transactionID].tsx +++ b/pages/[chainName]/[address]/transaction/[transactionID].tsx @@ -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

{ 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) { From 49fd0108fb5408d21d238057234ea017687ac27f Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:37:28 +0200 Subject: [PATCH 14/15] Adapt lib to new types --- lib/multisigHelpers.ts | 26 +++++++------------------- lib/txMsgHelpers.ts | 8 ++++---- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/lib/multisigHelpers.ts b/lib/multisigHelpers.ts index 8314161..5e04c95 100644 --- a/lib/multisigHelpers.ts +++ b/lib/multisigHelpers.ts @@ -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 }; diff --git a/lib/txMsgHelpers.ts b/lib/txMsgHelpers.ts index 2b176dd..97f84aa 100644 --- a/lib/txMsgHelpers.ts +++ b/lib/txMsgHelpers.ts @@ -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) { From 5a60780c39ee5a220a1cd29aea6477fa6519abb9 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Thu, 27 Jun 2024 11:38:05 +0200 Subject: [PATCH 15/15] Adapt components to new types --- components/dataViews/ListMultisigTxs.tsx | 149 ++++++++---------- components/dataViews/ListUserMultisigs.tsx | 24 +-- components/dataViews/ThresholdInfo.tsx | 4 +- .../dataViews/TransactionInfo/index.tsx | 4 +- components/forms/CreateTxForm/index.tsx | 18 +-- components/forms/TransactionSigning.tsx | 14 +- 6 files changed, 90 insertions(+), 123 deletions(-) diff --git a/components/dataViews/ListMultisigTxs.tsx b/components/dataViews/ListMultisigTxs.tsx index a3397d9..74bf8b6 100644 --- a/components/dataViews/ListMultisigTxs.tsx +++ b/components/dataViews/ListMultisigTxs.tsx @@ -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({

Loading transactions

) : null} + {walletInfo && transactions ? ( +
+ { + setShowBroadcasted(checked); + }} + /> + +
+ ) : 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 ? ( -
- { - setShowBroadcasted(checked); - }} - /> - -
- ) : null} -
- {((showBroadcasted ? transactions : pendingTxs) ?? []).map((tx) => { - const msgTypeCounts = msgTypeCountsFromJson(tx.dataJSON); - const hasSigned = Boolean( - tx.signatures.find(({ address }) => address === walletInfo?.address), - ); +
+ {((showBroadcasted ? transactions : pendingTxs) ?? []).map((tx) => { + const msgTypeCounts = msgTypeCountsFromJson(tx.dataJSON); + const hasSigned = Boolean( + tx.signatures.find(({ address }) => address === walletInfo?.address), + ); - return ( - - - - - {tx.signatures.length}/{multisigThreshold} + return ( + + + + + {tx.signatures.length}/{multisigThreshold} + + + +
+

signatures: {tx.signatures.length}

+

threshold: {multisigThreshold}

+

{hasSigned ? "Signed by you" : "Not signed by you"}

+

+ {tx.txHash + ? `Broadcasted with hash: ${ellideMiddle(tx.txHash, 12)}` + : "Not broadcasted"} +

+
+
+
+
+
+ {msgTypeCounts.map(({ msgType, count }) => ( + + {msgType} + {count > 1 ? ` (${count})` : ""} - - -
-

signatures: {tx.signatures.length}

-

threshold: {multisigThreshold}

-

{hasSigned ? "Signed by you" : "Not signed by you"}

-

- {tx.txHash - ? `Broadcasted with hash: ${ellideMiddle(tx.txHash, 12)}` - : "Not broadcasted"} -

-
-
- -
-
- {msgTypeCounts.map(({ msgType, count }) => ( - - {msgType} - {count > 1 ? ` (${count})` : ""} - - ))} -
-
-

{tx.id}

-
+ ))}
- - - ); - })} -
- +
+

{tx.id}

+
+
+ + + ); + })} +
) : null} diff --git a/components/dataViews/ListUserMultisigs.tsx b/components/dataViews/ListUserMultisigs.tsx index 96267c2..b6b4ca8 100644 --- a/components/dataViews/ListUserMultisigs.tsx +++ b/components/dataViews/ListUserMultisigs.tsx @@ -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({ diff --git a/components/dataViews/ThresholdInfo.tsx b/components/dataViews/ThresholdInfo.tsx index 8b093fa..c673ad0 100644 --- a/components/dataViews/ThresholdInfo.tsx +++ b/components/dataViews/ThresholdInfo.tsx @@ -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; } diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index e9c1857..97adca5 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -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) => { diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index b31f5a5..c6c9178 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -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({ diff --git a/components/forms/TransactionSigning.tsx b/components/forms/TransactionSigning.tsx index 1f61bd2..3fa6fa4 100644 --- a/components/forms/TransactionSigning.tsx +++ b/components/forms/TransactionSigning.tsx @@ -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 = { 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");