From 63e89507e75460182c113e3fd86a8eee6dbc116b Mon Sep 17 00:00:00 2001 From: Abhishek <64621806+growindiedev@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:43:04 +0530 Subject: [PATCH] render v3 spoils in spoils table (#148) * remove V2 data from accounting and fixed type and export errors * fetch smartInvoices and add query to fecth raids * create formatSpoils function and raids query mvp * unsuccessful attempt to fetch raids in useAccountingV3 * fetch invoice and raids and map them together using formatSpoils * render formatted spoils in spoilsTable * clean up useAccountingV3 & useFormattedDataV3 * cleanup accounting.tsx * change file names for useFormattedData to useFormattedAccountingV3 --- apps/frontend/components/SiteLayout.tsx | 7 +- apps/frontend/pages/accounting.tsx | 209 +++++------------- libs/dm-graphql/src/queries/accounting.ts | 14 ++ libs/dm-hooks/src/index.ts | 4 +- libs/dm-hooks/src/useAccountingV2.ts | 10 +- libs/dm-hooks/src/useAccountingV3.ts | 137 ++++++++++-- ...dDataV2.ts => useFormattedAccountingV2.ts} | 4 +- ...dDataV3.ts => useFormattedAccountingV3.ts} | 67 +++++- libs/dm-types/src/accounting.ts | 10 + libs/dm-utils/src/constants.ts | 5 +- 10 files changed, 279 insertions(+), 188 deletions(-) rename libs/dm-hooks/src/{useFormattedDataV2.ts => useFormattedAccountingV2.ts} (96%) rename libs/dm-hooks/src/{useFormattedDataV3.ts => useFormattedAccountingV3.ts} (81%) diff --git a/apps/frontend/components/SiteLayout.tsx b/apps/frontend/components/SiteLayout.tsx index 7f236e39..05eb9505 100644 --- a/apps/frontend/components/SiteLayout.tsx +++ b/apps/frontend/components/SiteLayout.tsx @@ -19,7 +19,7 @@ interface SiteLayoutProps { data?: any; subheader?: ReactNode; emptyDataPhrase?: string; - error?: Error; + error?: Error | boolean; isLoading?: boolean; minHeight?: string; } @@ -157,7 +157,10 @@ const SiteLayout = ({ minHeight={minHeight} > - Error loading data: {error.message} + + Error loading data + {typeof error === 'object' && `: ${error.message}`} + ); diff --git a/apps/frontend/pages/accounting.tsx b/apps/frontend/pages/accounting.tsx index 9014a8a0..75242ca8 100644 --- a/apps/frontend/pages/accounting.tsx +++ b/apps/frontend/pages/accounting.tsx @@ -10,18 +10,17 @@ import { Tabs, } from '@raidguild/design-system'; import { - useAccountingV2, - useFormattedDataV2, useMemberList, - useFormattedDataV3, + useFormattedAccountingV3, + useAccountingV3, } from '@raidguild/dm-hooks'; import { exportToCsv } from '@raidguild/dm-utils'; -import _ from 'lodash'; import { useSession } from 'next-auth/react'; import { NextSeo } from 'next-seo'; import Papa from 'papaparse'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; +import _ from 'lodash'; import BalancesTable from '../components/BalancesTable'; import SiteLayout from '../components/SiteLayout'; import SpoilsTable from '../components/SpoilsTable'; @@ -29,44 +28,29 @@ import TransactionsTable from '../components/TransactionsTable'; export const Accounting = () => { const { data: session } = useSession(); - const [isV3, setIsV3] = useState(true); const token = _.get(session, 'token'); - const { - data: dataFromMolochV2, - loading, - error, - } = useAccountingV2({ - token, - }); const { data: memberData } = useMemberList({ token, limit: 1000, }); - const { balances, spoils, transactions, tokenPrices } = dataFromMolochV2; + const { loading, isError, error } = useAccountingV3(); const { + formattedSpoils: formattedSpoilsV3, members, - balancesWithPrices: balancesWithPricesV2, - transactionsWithPrices: transactionsWithPricesV2, - transactionsWithPricesAndMembers: transactionsWithPricesAndMembersV2, - } = useFormattedDataV2(memberData, balances, transactions, tokenPrices); - - const { balancesWithPrices: balancesWithPricesV3, transactionsWithPrices: transactionsWithPricesV3, transactionsWithPricesAndMembers: transactionsWithPricesAndMembersV3, - } = useFormattedDataV3(memberData); + } = useFormattedAccountingV3(memberData); const onExportCsv = useCallback( (type: 'transactions' | 'balances' | 'spoils') => { let csvString = ''; if (type === 'transactions') { - const formattedTransactions = ( - isV3 ? transactionsWithPricesV3 : transactionsWithPricesV2 - ).map((t) => ({ + const formattedTransactions = transactionsWithPricesV3.map((t) => ({ Date: t.date, 'Tx Explorer Link': t.txExplorerLink, 'Elapsed Days': t.elapsedDays, @@ -99,9 +83,7 @@ export const Accounting = () => { csvString = Papa.unparse(formattedTransactions); } else if (type === 'balances') { if (type === 'balances') { - const formattedBalances = ( - isV3 ? balancesWithPricesV3 : balancesWithPricesV2 - ).map((b) => ({ + const formattedBalances = balancesWithPricesV3.map((b) => ({ Token: b.tokenSymbol, 'Tx Explorer Link': b.tokenExplorerLink, Inflow: b.inflow.tokenValue, @@ -126,7 +108,7 @@ export const Accounting = () => { csvString = Papa.unparse(formattedBalances); } } else if (type === 'spoils') { - const formattedSpoils = spoils.map((s) => ({ + const formattedSpoils = formattedSpoilsV3.map((s) => ({ Date: s.date, Raid: s.raidName, // TODO: Get this dynamically from the subgraph @@ -138,15 +120,7 @@ export const Accounting = () => { } exportToCsv(csvString, `raidguild-treasury-${type}`); }, - [ - balancesWithPricesV2, - balancesWithPricesV3, - isV3, - members, - spoils, - transactionsWithPricesV2, - transactionsWithPricesV3, - ] + [balancesWithPricesV3, members, formattedSpoilsV3, transactionsWithPricesV3] ); return ( @@ -156,14 +130,14 @@ export const Accounting = () => { Accounting} emptyDataPhrase='No transactions' - error={error} + error={error && isError} > @@ -180,133 +154,52 @@ export const Accounting = () => { - { - if (index === 0) setIsV3(true); - else setIsV3(false); - }} + - onExportCsv('balances')} + size='sm' + fontWeight='normal' > - - - V3 (current) - - - V2 - - - - - - - - - - - - - - + Export Balances + + + - { - if (index === 0) setIsV3(true); - else setIsV3(false); - }} + - onExportCsv('transactions')} + size='sm' + fontWeight='normal' > - - - V3 (current) - - - V2 - - - - - - - - - - - - - - + Export Transactions + + + - - onExportCsv('spoils')} + size='sm' + fontWeight='normal' > - - - V3 (current) - - - V2 - - - - - - - - - - -
This is the placeholder for v3 spoils data.
-
-
-
+ Export Spoils + + +
diff --git a/libs/dm-graphql/src/queries/accounting.ts b/libs/dm-graphql/src/queries/accounting.ts index fced3b19..ed9e8b8b 100644 --- a/libs/dm-graphql/src/queries/accounting.ts +++ b/libs/dm-graphql/src/queries/accounting.ts @@ -81,3 +81,17 @@ export const TRANSACTIONS_QUERY = gql` } } `; + +export const TRANSACTIONS_QUERY_V3 = gql` + query AccountingQuery { + raids( + where: { invoice_address: { _is_null: false } } + order_by: { created_at: desc } + ) { + id + invoice_address + name + created_at + } + } +`; diff --git a/libs/dm-hooks/src/index.ts b/libs/dm-hooks/src/index.ts index 09054a58..46695025 100644 --- a/libs/dm-hooks/src/index.ts +++ b/libs/dm-hooks/src/index.ts @@ -16,8 +16,8 @@ export { useContacts } from './useContacts'; export { default as useContactUpdate } from './useContactUpdate'; export { default as useDashboardList } from './useDashboardList'; export { default as useDefaultTitle } from './useDefaultTitle'; -export { default as useFormattedDataV2 } from './useFormattedDataV2'; -export { default as useFormattedDataV3 } from './useFormattedDataV3'; +export { default as useFormattedAccountingV2 } from './useFormattedAccountingV2'; +export { default as useFormattedAccountingV3 } from './useFormattedAccountingV3'; export * from './useLinks'; export { default as useLinksUpdate } from './useLinksUpdate'; export { default as useMemberCreate } from './useMemberCreate'; diff --git a/libs/dm-hooks/src/useAccountingV2.ts b/libs/dm-hooks/src/useAccountingV2.ts index 4e99af1c..4f19af53 100644 --- a/libs/dm-hooks/src/useAccountingV2.ts +++ b/libs/dm-hooks/src/useAccountingV2.ts @@ -19,7 +19,7 @@ import { camelize, formatDate, formatUnitsAsNumber, - GUILD_GNOSIS_DAO_ADDRESS, + GUILD_GNOSIS_DAO_ADDRESS_V2, } from '@raidguild/dm-utils'; import { useInfiniteQuery } from '@tanstack/react-query'; import _ from 'lodash'; @@ -197,7 +197,7 @@ const formatBalancesAsTransactions = async ( const txExplorerLink = `https://blockscout.com/xdai/mainnet/tx/${molochStatBalance.transactionHash}`; const proposalLink = molochStatBalance.proposalDetail - ? `https://app.daohaus.club/dao/0x64/${GUILD_GNOSIS_DAO_ADDRESS}/proposals/${molochStatBalance.proposalDetail.proposalId}` + ? `https://app.daohaus.club/dao/0x64/${GUILD_GNOSIS_DAO_ADDRESS_V2}/proposals/${molochStatBalance.proposalDetail.proposalId}` : ''; const epochTimeAtIngressMs = Number(molochStatBalance.timestamp) * 1000; @@ -358,9 +358,9 @@ export const useAccountingV2 = ({ token }: { token: string }) => { const response = await client({ token }).request(TRANSACTIONS_QUERY, { first: limit, skip: pageParam * limit, - molochAddress: GUILD_GNOSIS_DAO_ADDRESS, - contractAddr: GUILD_GNOSIS_DAO_ADDRESS, - escrowParentAddress: GUILD_GNOSIS_DAO_ADDRESS, + molochAddress: GUILD_GNOSIS_DAO_ADDRESS_V2, + contractAddr: GUILD_GNOSIS_DAO_ADDRESS_V2, + escrowParentAddress: GUILD_GNOSIS_DAO_ADDRESS_V2, }); return { diff --git a/libs/dm-hooks/src/useAccountingV3.ts b/libs/dm-hooks/src/useAccountingV3.ts index c6e8dc00..c2779cae 100644 --- a/libs/dm-hooks/src/useAccountingV3.ts +++ b/libs/dm-hooks/src/useAccountingV3.ts @@ -1,10 +1,24 @@ /* eslint-disable no-await-in-loop */ -import { Proposal, RageQuit } from '@raidguild/dm-types'; -import { GNOSIS_SAFE_ADDRESS } from '@raidguild/dm-utils'; +import { + client as dmGraphQlClient, + TRANSACTIONS_QUERY_V3, +} from '@raidguild/dm-graphql'; +import { + IAccountingRaid, + Invoice, + Proposal, + RageQuit, +} from '@raidguild/dm-types'; +import { + camelize, + GNOSIS_SAFE_ADDRESS, + GUILD_GNOSIS_DAO_ADDRESS_V3, +} from '@raidguild/dm-utils'; import { NETWORK_CONFIG } from '@raidguild/escrow-utils'; -import { useQueries, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query'; import { GraphQLClient } from 'graphql-request'; import _ from 'lodash'; +import { useSession } from 'next-auth/react'; import { getAddress } from 'viem'; const graphUrl = (chainId: number = 4) => @@ -89,28 +103,103 @@ const getRageQuits = async (v3client: GraphQLClient): Promise => { } }; +const getSmartInvoice = async ( + v3ClientInvoices: GraphQLClient +): Promise => { + try { + const invoices: { invoices: Invoice[] } = await v3ClientInvoices.request(` + { + invoices (where: { provider: "${GUILD_GNOSIS_DAO_ADDRESS_V3}" }) { + token + address + releases { + timestamp + amount + } + } + } + `); + + return invoices.invoices; + } catch (error) { + return []; + } +}; + +const raidsQueryResult = async (token: string) => { + const response = await dmGraphQlClient({ token }).request( + TRANSACTIONS_QUERY_V3 + ); + + return { + raids: camelize(_.get(response, 'raids')), + }; +}; + const useAccountingV3 = () => { + const { data: session } = useSession(); + const token = _.get(session, 'token') as string; + const checksum = getAddress(GNOSIS_SAFE_ADDRESS); const v3client = new GraphQLClient( `https://gateway-arbitrum.network.thegraph.com/api/${process.env.NEXT_PUBLIC_THE_GRAPH_API_KEY}/subgraphs/id/6x9FK3iuhVFaH9sZ39m8bKB5eckax8sjxooBPNKWWK8r` ); - const { data: tokenBalances, error: tokenBalancesError } = useQuery( - ['tokenBalances', checksum], - () => listTokenBalances({ safeAddress: checksum }) + const v3ClientInvoices = new GraphQLClient( + `https://api.studio.thegraph.com/proxy/78711/smart-invoice-gnosis/v0.0.1/` ); - const { data: txResponse, error: txResponseError } = useQuery( - ['transactions', checksum], - () => getSafeTransactionProposals({ safeAddress: checksum }) + const { + isError: raidsIsError, + isLoading: raidsIsLoading, + error: raidsError, + data: raidsData, + } = useInfiniteQuery< + { + raids: Array; + }, + Error + >(['raidsV3'], () => raidsQueryResult(token), { + getNextPageParam: (lastPage, allPages) => + _.isEmpty(lastPage) + ? undefined + : _.divide(_.size(_.flatten(allPages)), 100), + enabled: Boolean(token), + }); + + const { + data: tokenBalances, + error: tokenBalancesError, + isLoading: tokenBalancesLoading, + isError: tokenBalancesIsError, + } = useQuery(['tokenBalances', checksum], () => + listTokenBalances({ safeAddress: checksum }) ); - const { data: rageQuitsData, error: rageQuitsError } = useQuery( - ['rageQuits'], - () => getRageQuits(v3client) + const { + data: txResponse, + error: txResponseError, + isLoading: txResponseLoading, + isError: txResponseIsError, + } = useQuery(['transactions', checksum], () => + getSafeTransactionProposals({ safeAddress: checksum }) ); + const { + data: rageQuitsData, + error: rageQuitsError, + isLoading: rageQuitsDataLoading, + isError: rageQuitsIsError, + } = useQuery(['rageQuits'], () => getRageQuits(v3client)); + + const { + data: smartInvoiceData, + error: smartInvoiceError, + isLoading: smartInvoiceLoading, + isError: smartInvoiceIsError, + } = useQuery(['smartInvoice'], () => getSmartInvoice(v3ClientInvoices)); + const proposalQueries = _.map(txResponse?.txData, (tx) => { const txHash = tx.transactionHash || tx.txHash; @@ -151,8 +240,24 @@ const useAccountingV3 = () => { }) || []; const proposalsInfo = useQueries({ queries: proposalQueries }); - - const error = tokenBalancesError || txResponseError || rageQuitsError; + const error = + tokenBalancesError || + txResponseError || + rageQuitsError || + smartInvoiceError || + raidsError; + const isError = + tokenBalancesIsError || + txResponseIsError || + rageQuitsIsError || + smartInvoiceIsError || + raidsIsError; + const loading = + tokenBalancesLoading || + txResponseLoading || + rageQuitsDataLoading || + smartInvoiceLoading || + raidsIsLoading; const transformProposals = proposalsInfo .filter((query) => query.data) .map((query) => query.data as Proposal) @@ -163,13 +268,15 @@ const useAccountingV3 = () => { }, {} as Record>); const data = { + raids: raidsData?.pages[0].raids, + smartInvoice: smartInvoiceData, tokens: tokenBalances?.data, transactions: txResponse?.txData, rageQuits: rageQuitsData || [], proposalsInfo: transformProposals, }; - return { data, error }; + return { data, error, isError, loading }; }; export default useAccountingV3; diff --git a/libs/dm-hooks/src/useFormattedDataV2.ts b/libs/dm-hooks/src/useFormattedAccountingV2.ts similarity index 96% rename from libs/dm-hooks/src/useFormattedDataV2.ts rename to libs/dm-hooks/src/useFormattedAccountingV2.ts index 994ac452..71dd08be 100644 --- a/libs/dm-hooks/src/useFormattedDataV2.ts +++ b/libs/dm-hooks/src/useFormattedAccountingV2.ts @@ -9,7 +9,7 @@ import { InfiniteData } from '@tanstack/react-query'; import _ from 'lodash'; import { useCallback, useMemo } from 'react'; -const useFormattedDataV2 = ( +const useFormattedAccountingV2 = ( memberData: InfiniteData, balances: ITokenBalanceLineItem[], transactions: IVaultTransaction[], @@ -84,4 +84,4 @@ const useFormattedDataV2 = ( }; }; -export default useFormattedDataV2; +export default useFormattedAccountingV2; diff --git a/libs/dm-hooks/src/useFormattedDataV3.ts b/libs/dm-hooks/src/useFormattedAccountingV3.ts similarity index 81% rename from libs/dm-hooks/src/useFormattedDataV3.ts rename to libs/dm-hooks/src/useFormattedAccountingV3.ts index 88ab8c66..6f5407e9 100644 --- a/libs/dm-hooks/src/useFormattedDataV3.ts +++ b/libs/dm-hooks/src/useFormattedAccountingV3.ts @@ -1,6 +1,9 @@ /* eslint-disable no-param-reassign */ import { + IAccountingRaid, IMember, + Invoice, + ISpoils, ITokenBalanceLineItemV3, IVaultTransactionV2, } from '@raidguild/dm-types'; @@ -12,7 +15,7 @@ import { } from '@raidguild/dm-utils'; import { InfiniteData } from '@tanstack/react-query'; import _ from 'lodash'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import useAccountingV3 from './useAccountingV3'; @@ -78,12 +81,61 @@ function calculateTokenFlows(transactions: IVaultTransactionV2[]) { return tokenBalances.getBalances(); } -const useFormattedDataV3 = (memberData: InfiniteData) => { +const formatSpoils = async ( + Raids: IAccountingRaid[], + Invoices: Invoice[] +): Promise => { + const wxDAI = '0xe91d153e0b41518a2ce8dd3d7944fa863463a97d'; + const [raids, invoices] = await Promise.all([Raids, Invoices]); + + const filteredInvoices = invoices.filter( + (invoice) => invoice.token === wxDAI + ); + + const spoils = raids + .map((raid) => { + const invoice = filteredInvoices.find( + (inv) => + inv.address?.toLowerCase() === raid.invoiceAddress?.toLowerCase() + ); + + if (!invoice) return null; + + const totalReleased = invoice.releases.reduce( + (acc, release) => acc + formatUnitsAsNumber(release.amount, 18), + 0 + ); + + const latestTimestamp = Math.max( + ...invoice.releases.map((release) => Number(release.timestamp)) + ); + + const spoilsAmount = totalReleased * 0.1; + const childShare = totalReleased - spoilsAmount; + + return { + raidLink: `/raids/${raid.id}`, + raidName: raid.name, + childShare, + parentShare: spoilsAmount, + priceConversion: 1, + date: new Date(latestTimestamp * 1000), + tokenSymbol: 'wxDAI', + }; + }) + .filter((spoil) => spoil !== null); + return spoils.sort((a, b) => b.date.getTime() - a.date.getTime()); +}; + +const useFormattedAccountingV3 = (memberData: InfiniteData) => { + const [formattedSpoils, setFormattedSpoils] = useState([]); const { data: dataFromMolochV3 } = useAccountingV3(); const balances = dataFromMolochV3?.tokens?.tokenBalances || []; const transactions = dataFromMolochV3?.transactions || []; const proposalsInfo = dataFromMolochV3?.proposalsInfo || {}; const rageQuits = dataFromMolochV3?.rageQuits || []; + const raids = dataFromMolochV3?.raids || []; + const invoices = dataFromMolochV3?.smartInvoice || []; const flows = useMemo( () => calculateTokenFlows(transactions), @@ -97,6 +149,13 @@ const useFormattedDataV3 = (memberData: InfiniteData) => { return _.keyBy(memberArray, (m: IMember) => m.ethAddress?.toLowerCase()); }, [memberData]); + useMemo(() => { + (async () => { + const spoils = await formatSpoils(raids, invoices); + setFormattedSpoils(spoils); + })(); + }, [raids, invoices]); + const withPrices = useCallback( (items: T[]) => _.map(items, (t) => { @@ -267,10 +326,12 @@ const useFormattedDataV3 = (memberData: InfiniteData) => { ); return { + members, + formattedSpoils, balancesWithPrices, transactionsWithPrices, transactionsWithPricesAndMembers, }; }; -export default useFormattedDataV3; +export default useFormattedAccountingV3; diff --git a/libs/dm-types/src/accounting.ts b/libs/dm-types/src/accounting.ts index 49c25e0f..ae68739f 100644 --- a/libs/dm-types/src/accounting.ts +++ b/libs/dm-types/src/accounting.ts @@ -216,6 +216,7 @@ export type TransferV3 = { export type ITokenBalanceLineItemV3 = (ITokenBalanceV3 | TransferV3) & { id: string; tokenExplorerLink: string; + tokenSymbol: string; inflow: { tokenValue: bigint; }; @@ -244,3 +245,12 @@ export interface RageQuit { shares: string; txHash: string; } + +export interface Invoice { + token: string; + address?: string; + releases: { + timestamp: string; + amount: bigint; + }[]; +} diff --git a/libs/dm-utils/src/constants.ts b/libs/dm-utils/src/constants.ts index 0e676c9d..1c1f3730 100644 --- a/libs/dm-utils/src/constants.ts +++ b/libs/dm-utils/src/constants.ts @@ -290,9 +290,12 @@ export const GUILD_CLASS_DISPLAY = { ACCOUNT_MANAGER: 'Cleric (Client Manager)', }; -export const GUILD_GNOSIS_DAO_ADDRESS = +export const GUILD_GNOSIS_DAO_ADDRESS_V2 = '0xfe1084bc16427e5eb7f13fc19bcd4e641f7d571f'; +export const GUILD_GNOSIS_DAO_ADDRESS_V3 = + '0xf02fd4286917270cb94fbc13a0f4e1ed76f7e986'; + export const GNOSIS_SAFE_ADDRESS = '0x181eBDB03cb4b54F4020622F1B0EAcd67A8C63aC'; export const SIDEBAR_ACTION_STATES = {