From c9c34afe12e817cee1160cf4391f7807dd9ab777 Mon Sep 17 00:00:00 2001 From: Kris Bitney Date: Fri, 19 Jan 2024 18:18:31 +0500 Subject: [PATCH] Recent transactions list (#114) * Initial re-work UI for desktop based on latest changes * deps * deps * Add typeform to create a good collective button * Wagmi integration * conditional render of address or ens. Truncate address shown * Desktop * wagmi, routing, superfluid * wagmi, routing, superfluid * Fix currency * Update develop format * wagmi, routing, superfluid * Dynamic data template for payment pools * supporter and steward * Add missing param in event * viaIr, new events * Integrated Real Time Data * wagmi fix all data hooked up * wagmi fix all data hooked up * yarn lock * fix: yarn lock * fix: restore master yarn lock * feat: btn * fix: sdk build * fix: esm export * Working through SuperToken Initialization Error: There was an error initializing the SuperToken, last part needed it to get the contract fucntion to pass * Done * . * fix: hardhat lint * add: override reward * add: deployed upgraded pool with override * fix: contracts types build * fix: ethers typescript errors * fix: types * update * updated subgraph * Put back functions lost * fix: deploy preview on udpates to PR * wip: test preview yml * fix: lock file * Update donor * add: tsc lint-staged * fix: lock issue * fix: typescript jsx transform * Update donor * . * fix: unit tests * . * update * Fix: build * fix: unit tests * fix: build correctly * wip: fix PR test * Update readme to clarify that packages must be built locally * Attempt to port existing desktop wallet connection handling to mobile * fix: event quantity * refactor: package.json * Point to working subgraph * refactored header and changed wallet connect buttons layout to menu * added todo item * fixed wallet connector dropdown offset * animated wallet connector menu button arrow * Collective Home Card description is now limited to 3 lines and expands on click * reduced width of desktop home collective card containers so they more closely resemble the mobile card containers * fixed padding on ViewCollective page and did some touchups on stewards list * fixed CTA button size on DonationPage * fixed Breadcrumb on DonationPage * fixed icon alignment on ViewCollective page * added random profile pics for stewards * fixes for layout of ViewStewards page on mobile * small refactors while trying to figure out why "see all stewards" button isn't working on desktop * set gap between stewards and transactions to 32 on desktop ViewCollective page * refactored subgraph data fetching * fix: empty arrays, relations * wip: subgraph fixes * additional refactoring of models * small progress made in adding dynamic data * reworked subgraph; added entities for StewardCollective, Reward, DonorCollective, and Donation * updated pool creation in subgraph * adjusted Collective schema * made all fields non-nullable since they all have values when instantiated * removed 'contributions' summary value from Collective * added summary statistics to Collective * updated contribution calculations for donors in subgraph * added dev comments in schema for clarity * reworked subgraph queries and model transforms (build broken) * added dynamic data to ViewCollective and ViewStewards (build broken) * updated wallet profile page * removed unused pages and fixed build * added ipfs data parsing and removed unused reward/donation code * fixed ipfs collective id * Add: Design system (#54) * add: design system, svg support * add: svg default exports, apply svg icons to ViewCollective * - add withTheme hook for the AboutCard - add theme also to native provider * fix: apply styles for aboutcard better, add readme * fix: forgot to comment out web3modal * chore: bump good-design * Update README.md * front end now obtains ipfs data from subgraph instead of fetching directly from ipfs * minor refactoring * made event-claim relationship m:1 * fix: exluded deps from bundle * fix for desktop padding on view collective page * added tsconfig for assemblyscript in subgraph package * updated ViewDonorsPage and its components * added correct Collective metadata to donate page * added ipfs collective metadata update to handlePoolDetailsChanged * changed variable name 'nftAddress' to 'nftId' in subgraph pool.ts; changed derivedFrom relationships in collective * removed deriveFrom relationship from eventdata claim * reconfigured subgraph to recognize factory-template relationship between poolfactory and pool * fix: subgraph null, ipfs, mappings file * ensured required values in subgraph entities are populated * update yarn.lock * fixed assemblyscript config extension path * updated DonateComponent to determine if balance is sufficient; removed @constants dir * added donation timestamp to DonorCollective (needed for superfluid real-time updates) * added donation timestamp to DonorCollective (needed for superfluid real-time updates) * removed missed usages of @constants folder * fix: yarn lock * displaying realtime superfluid donations (untested) * fixed null values in handleRewardClaim NFT * updated yarn lock * updated yarn lock * using hex string for pool project id due to invalid utf-8 data * removed ipfs retries since they don't seem to help * fixed Claim totalRewards initialization * reworked ipfs usage in subgraph to use new data source feature * updated data flow to accomodate subgraph updates * add required fields to ipfs entity in subgraph * adjusted ipfs fetching in front end * renamed IpfsCollective.address to IpfsCollective.collective * removed IpfsCollective.collective * fixes to subgraph ipfs fetching in front end * removed several hard-coded SVG strings * several minor fixes * various minor fixes * reworked wallet profile UI * removed use of `window` * refactored useGetBalance * post-merge fixes * removed unused constants * removed redundant view from ViewStewardsPage.tsx * removed unused styles from ViewStewardsPage.tsx * removed unused styles from ViewDonorsPage.tsx * ui fixes ViewStewardsPage and ViewDonorsPage * removed unused import and console log * updated useContractCalls and much of DonateComponent * fixed double usage of insufficientLiquidity * simplified DonorListItem ranks * ProfileView refactor * added useApproveSwapTokenCallback and useSwapRoute * removed mock data * draft swap integration * added index to hooks/useContractCalls * prevented swap from G$ to G$ * fixed formatting and some swap bugs * removed unused function from lib * added flowing balances for total donation amounts for collectives and donors * added useTokenList hook * now using celo token list * removed unused import * made dropdown menu scrollable * added storage cache to apollo client * swap works! * adjustments to reflect uniswap default slippage tolerance * fixed jsbi via vite configuration * changed priceImpact to number * updated default apollo client fetch policy to 'cache-and-network' * removed unused `WalletConnectionProvider` context * by request, handleApproveToken is no longer memoized * fixed GD formatting in Donor List * changed acceptable price impact percent to 5% and moved it to constants.ts * add swap and fix donate (#72) * updated useContractCalls and much of DonateComponent * added useApproveSwapTokenCallback and useSwapRoute * draft swap integration * added index to hooks/useContractCalls * prevented swap from G$ to G$ * fixed formatting and some swap bugs * removed unused function from lib * now using celo token list * removed unused import * made dropdown menu scrollable * swap works! * adjustments to reflect uniswap default slippage tolerance * fixed jsbi via vite configuration * changed priceImpact to number * removed unused `WalletConnectionProvider` context * by request, handleApproveToken is no longer memoized * fixed GD formatting in Donor List * changed acceptable price impact percent to 5% and moved it to constants.ts * added steward verification check * added currentPool value fetching * added "people supported" counts * fixed for useIsStewardVerified and added FlowingDonationsRowItem to reduce re-renders * fixes for currentPool, donor list * fix for ens name support * Bug fixes (#111) * fix social links and show only if available * changed DonorListItem amount display from toFixed to toString * sort connectors with WalletConnect before MetaMask * added token symbol next to wallet balance and adjusted balance formatting * update copy for on-time donation, added chain check to useEthersSigner and useGetTokenBalance * one-time transfer now assumes token is already approved for transfer * G$ donation now assumes token is already approved for transfer * added infoButton property to ipfs meta data in subgraph * increased slippage tolerance from 0.5% to 1% * made it so only G$ shows in token list for one-time donations * made it so selecting one-time donation changes currency to G$ and amount to 0 * fixed total donation usd value * swap errors will only show if donation amount is not zero * added infoLabel to front end * updated breadcrumb to show collective name and only go back to previous page if back arrow is clicked * updated breadcrumb to fix links and text * incremented subgraph starting block because prior block was failing * donation is now disabled if swap required and token approval not yet ready; token approval should not fail on retry * breadcrumb formatting for '/' * adjusted G$ formatting in profile * modal and donation error formatting * added steward profile link to steward list item * added polling to donor in wallet; improved error message in donation functions * fix for error message in donation functions * switched from supportSingleTransferAndCall to supportSingleBatch since it includes token approval * removed warnings for missing subgraph items * set margin between collective cards on home page to 24 * fixed transaction list UI * added useRecentTransactions hook * fixed ViewCollective tablet portrait resolution * removed G$ fractional amount from connected wallet display, and added currency symbol to mobile version * made sure G$ displayed amount is rounded down to nearest int * fixed stop donation * changed default gas price to 5 in @gooddollar/goodcollective-sdk * renamed enum value from celo to CELO * currentPool is now a flowing balance * refactor: dry up donate button styles, no inline arrow functions * fix: getDonateStyles, does not stop loop after hitting first true * fix: simplify getDonateStyles * inline lambdas to const * memoized onChangeFrequency * fixed link to smart contract in ViewCollective * version bump * version bump * version bump * version bump * version bump * version bump * version bump * version bump * version bump * chore: release sdk, bump in app --------- Co-authored-by: LewisB * added SupportEvent to subgraph * Added collective id to Claim in subgraph * added donor and collective to SupportEvent in subgraph * fix for subgraph * changed Claim id to tx hash in subgraph * changed Claim back to event uri in subgraph; added txHash to Claim instead * added network fee as an optional parameter in subgraph event entities * added timestamp to Claim in subgraph * changed timestamp types to Int in subgraph * updated transaction list and added hooks to fetch transactions * updated subgraph network fee properties * fixes for transaction fetching * updated tx hash formatting to match design spec * removed OK button from CompleteDonationModal and fixed image height in empty profile * adjusted poll intervals * added thank you modal and stop donation modal to desktop ViewCollective * fixed thank you modal behavior --------- Co-authored-by: Luis Castillo Co-authored-by: Joshua-Jack Co-authored-by: sirpy Co-authored-by: LewisB Co-authored-by: Ben Walker Co-authored-by: sirpy Co-authored-by: Lewis B --- packages/app/src/App.tsx | 3 -- .../app/src/components/DonateComponent.tsx | 10 ++-- .../ClaimTransactionListItem.tsx | 36 +++++++++++++ .../SupportTransactionListItem.tsx | 41 ++++++++++++++ .../TransactionList/TransactionList.tsx | 20 ++++--- .../TransactionList/TransactionListItem.tsx | 48 +++++++---------- .../app/src/components/ViewCollective.tsx | 12 +++-- .../components/WalletDetails/EmptyProfile.tsx | 2 +- .../{ => modals}/ApproveSwapModal.tsx | 40 +++++++------- .../{ => modals}/CompleteDonationModal.tsx | 36 ++++++------- .../components/{ => modals}/ErrorModal.tsx | 40 +++++++------- .../{ => modals}/StopDonationModal.tsx | 37 ++++++------- .../components/{ => modals}/SwitchModal.tsx | 42 +++++++-------- .../components/{ => modals}/ThankYouModal.tsx | 49 ++++++++--------- .../useContractCalls/useContractCalls.tsx | 11 ++-- .../hooks/useContractCalls/useSupportFlow.ts | 15 ++++-- .../useSupportFlowWithSwap.ts | 18 ++++--- .../useContractCalls/useSupportSingleBatch.ts | 15 ++++-- .../useSupportSingleTransferAndCall.ts | 15 ++++-- .../app/src/hooks/useRecentTransactions.ts | 47 +++++++++++----- packages/app/src/models/models.ts | 20 +++++-- packages/app/src/models/transforms.ts | 44 +++++++++++++-- packages/app/src/models/typeUtil.ts | 9 ++++ packages/app/src/pages/ModalTestPage.tsx | 24 --------- packages/app/src/pages/WalletProfilePage.tsx | 2 +- packages/app/src/subgraph/index.ts | 2 + packages/app/src/subgraph/subgraphModels.ts | 47 +++++++++++----- packages/app/src/subgraph/useSubgraphClaim.ts | 53 +++++++++++++++++++ .../app/src/subgraph/useSubgraphCollective.ts | 2 +- packages/app/src/subgraph/useSubgraphData.ts | 4 ++ packages/app/src/subgraph/useSubgraphDonor.ts | 2 +- .../src/subgraph/useSubgraphIpfsCollective.ts | 2 +- .../src/subgraph/useSubgraphSupportEvent.ts | 46 ++++++++++++++++ packages/subgraph/schema.graphql | 34 +++++++++--- packages/subgraph/src/mappings/pool.ts | 30 +++++------ packages/subgraph/src/mappings/poolFactory.ts | 2 +- packages/subgraph/src/mappings/superApp.ts | 20 +++++-- 37 files changed, 590 insertions(+), 290 deletions(-) create mode 100644 packages/app/src/components/TransactionList/ClaimTransactionListItem.tsx create mode 100644 packages/app/src/components/TransactionList/SupportTransactionListItem.tsx rename packages/app/src/components/{ => modals}/ApproveSwapModal.tsx (63%) rename packages/app/src/components/{ => modals}/CompleteDonationModal.tsx (62%) rename packages/app/src/components/{ => modals}/ErrorModal.tsx (65%) rename packages/app/src/components/{ => modals}/StopDonationModal.tsx (63%) rename packages/app/src/components/{ => modals}/SwitchModal.tsx (98%) rename packages/app/src/components/{ => modals}/ThankYouModal.tsx (55%) create mode 100644 packages/app/src/models/typeUtil.ts delete mode 100644 packages/app/src/pages/ModalTestPage.tsx create mode 100644 packages/app/src/subgraph/useSubgraphClaim.ts create mode 100644 packages/app/src/subgraph/useSubgraphSupportEvent.ts diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index e3f907dd..dd773e6e 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -13,7 +13,6 @@ import * as WebRoute from './routes/routing.web'; import ActivityLogPage from './pages/ActivityLogPage'; import { Providers } from './Providers'; import DonatePage from './pages/DonatePage'; -import ModalTestPage from './pages/ModalTestPage'; import { configureChains, createConfig, WagmiConfig } from 'wagmi'; import { celo, mainnet } from 'wagmi/chains'; import { publicProvider } from 'wagmi/providers/public'; @@ -95,7 +94,6 @@ function App(): JSX.Element { } /> } /> } /> - } /> } /> @@ -113,7 +111,6 @@ function App(): JSX.Element { } /> } /> } /> - } /> )} diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 500d039e..82c184dc 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react'; import { Image, StyleSheet, Text, TextInput, View } from 'react-native'; import { InterRegular, InterSemiBold, InterSmall } from '../utils/webFonts'; import RoundedButton from './RoundedButton'; -import CompleteDonationModal from './CompleteDonationModal'; +import CompleteDonationModal from './modals/CompleteDonationModal'; import { Colors } from '../utils/colors'; import { Link, useMediaQuery } from 'native-base'; import Dropdown from './Dropdown'; @@ -16,14 +16,15 @@ import { InfoIconOrange } from '../assets'; import { useLocation } from 'react-router-native'; import Decimal from 'decimal.js'; import { formatFiatCurrency } from '../lib/formatFiatCurrency'; -import ErrorModal from './ErrorModal'; +import ErrorModal from './modals/ErrorModal'; import { SwapRouteState, useSwapRoute } from '../hooks/useSwapRoute'; import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback'; -import ApproveSwapModal from './ApproveSwapModal'; +import ApproveSwapModal from './modals/ApproveSwapModal'; import { waitForTransaction } from '@wagmi/core'; import { TransactionReceipt } from 'viem'; import { useToken, useTokenList } from '../hooks/useTokenList'; import { formatDecimalStringInput } from '../lib/formatDecimalStringInput'; +import ThankYouModal from './modals/ThankYouModal'; interface DonateComponentProps { collective: IpfsCollective; @@ -42,6 +43,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const [completeDonationModalVisible, setCompleteDonationModalVisible] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false); + const [thankYouModalVisible, setThankYouModalVisible] = useState(false); const [currency, setCurrency] = useState('G$'); const [frequency, setFrequency] = useState(Frequency.OneTime); @@ -84,6 +86,7 @@ function DonateComponent({ collective }: DonateComponentProps) { frequency, (error) => setErrorMessage(error), (value: boolean) => setCompleteDonationModalVisible(value), + (value: boolean) => setThankYouModalVisible(value), rawMinimumAmountOut, swapPath ); @@ -473,6 +476,7 @@ function DonateComponent({ collective }: DonateComponentProps) { + ); } diff --git a/packages/app/src/components/TransactionList/ClaimTransactionListItem.tsx b/packages/app/src/components/TransactionList/ClaimTransactionListItem.tsx new file mode 100644 index 00000000..baa6e714 --- /dev/null +++ b/packages/app/src/components/TransactionList/ClaimTransactionListItem.tsx @@ -0,0 +1,36 @@ +import { ClaimTx } from '../../models/models'; +import { formatAddress } from '../../lib/formatAddress'; +import { useEnsName } from 'wagmi'; +import TransactionListItem from './TransactionListItem'; + +interface ClaimTransactionListItemProps { + transaction: ClaimTx; +} + +export function ClaimTransactionListItem({ transaction }: ClaimTransactionListItemProps) { + const { hash, networkFee, stewards, totalRewards } = transaction; + + let multipleStewardsText: string | undefined; + let userAddress: `0x${string}` | undefined; + + if (stewards.length === 1) { + userAddress = stewards[0] as `0x${string}`; + } else { + multipleStewardsText = `${stewards.length} Stewards`; + } + + const { data: ensName } = useEnsName({ address: userAddress, chainId: 1 }); + // TODO: how to get first name and last name of users? + const userIdentifier = multipleStewardsText ?? ensName ?? (userAddress ? formatAddress(userAddress) : 'Unknown'); + + return ( + + ); +} diff --git a/packages/app/src/components/TransactionList/SupportTransactionListItem.tsx b/packages/app/src/components/TransactionList/SupportTransactionListItem.tsx new file mode 100644 index 00000000..1ef8b29d --- /dev/null +++ b/packages/app/src/components/TransactionList/SupportTransactionListItem.tsx @@ -0,0 +1,41 @@ +import { SupportTx } from '../../models/models'; +import { formatAddress } from '../../lib/formatAddress'; +import { useEnsName } from 'wagmi'; +import Decimal from 'decimal.js'; +import TransactionListItem from './TransactionListItem'; +import { useFlowingBalance } from '../../hooks/useFlowingBalance'; + +interface SupportTransactionListItemProps { + transaction: SupportTx; +} + +export function SupportTransactionListItem({ transaction }: SupportTransactionListItemProps) { + const { hash, networkFee, timestamp, donor } = transaction; + + // TODO: how to get first name and last name of users? + const userFullName: string | undefined = undefined; + const userAddress = donor as `0x${string}`; + const { data: ensName } = useEnsName({ address: userAddress, chainId: 1 }); + const userIdentifier = userFullName ?? ensName ?? formatAddress(userAddress); + + const { formatted: formattedAmount } = useFlowingBalance( + transaction.contribution, + timestamp, + transaction.flowRate, + undefined + ); + const amount = transaction.isFlowUpdate + ? formattedAmount + : new Decimal(transaction.contribution).minus(transaction.previousContribution).toString(); + + return ( + + ); +} diff --git a/packages/app/src/components/TransactionList/TransactionList.tsx b/packages/app/src/components/TransactionList/TransactionList.tsx index d09f57de..dd8f261d 100644 --- a/packages/app/src/components/TransactionList/TransactionList.tsx +++ b/packages/app/src/components/TransactionList/TransactionList.tsx @@ -1,12 +1,14 @@ import { Image, Text, View, StyleSheet, TouchableOpacity } from 'react-native'; import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import TransactionListItem from './TransactionListItem'; import { Colors } from '../../utils/colors'; import { useMediaQuery } from 'native-base'; import { chevronDown, TransactionIcon } from '../../assets'; -import { Transaction } from '../../models/models'; +import { ClaimTx, Transaction } from '../../models/models'; import useCrossNavigate from '../../routes/useCrossNavigate'; import { useRecentTransactions } from '../../hooks/useRecentTransactions'; +import { isSupportTx } from '../../models/typeUtil'; +import { ClaimTransactionListItem } from './ClaimTransactionListItem'; +import { SupportTransactionListItem } from './SupportTransactionListItem'; interface TransactionListProps { collective: `0x${string}`; @@ -18,7 +20,7 @@ function TransactionList({ collective }: TransactionListProps) { }); const { navigate } = useCrossNavigate(); - const transactions: Transaction[] = useRecentTransactions(collective, 6); + const transactions: Transaction[] = useRecentTransactions(collective, 6, 1000); const onClickShowMore = () => navigate('/profile/abc123/activity'); @@ -30,9 +32,15 @@ function TransactionList({ collective }: TransactionListProps) { {isDesktopResolution && } - {transactions.slice(0, 5).map((transaction) => ( - - ))} + {transactions + .slice(0, 5) + .map((transaction) => + isSupportTx(transaction) ? ( + + ) : ( + + ) + )} {isDesktopResolution && transactions.length > 5 && ( diff --git a/packages/app/src/components/TransactionList/TransactionListItem.tsx b/packages/app/src/components/TransactionList/TransactionListItem.tsx index 63d24ab1..d6c5ed50 100644 --- a/packages/app/src/components/TransactionList/TransactionListItem.tsx +++ b/packages/app/src/components/TransactionList/TransactionListItem.tsx @@ -2,46 +2,38 @@ import { Image, Text, View, StyleSheet } from 'react-native'; import { InterRegular, InterSemiBold } from '../../utils/webFonts'; import { Colors } from '../../utils/colors'; import { ReceiveIcon, SendIcon } from '../../assets'; -import { Transaction } from '../../models/models'; -import { formatAddress } from '../../lib/formatAddress'; -import { useEnsName } from 'wagmi'; import Decimal from 'decimal.js'; import { ethers } from 'ethers'; interface TransactionListItemProps { - collective: `0x${string}`; - transaction: Transaction; + userIdentifier: string; + isDonation?: boolean; + amount: string; + amountIsFormatted?: boolean; + txHash: string; + rawNetworkFee: string; } -function TransactionListItem({ collective, transaction }: TransactionListItemProps) { - const { hash, rawAmount, from, to, fee } = transaction; - - const donation = to === collective; - - const userAddress: `0x${string}` = (donation ? from : to) as `0x${string}`; - const { data: ensName } = useEnsName({ address: userAddress, chainId: 1 }); - // TODO: how to get first name and last name of users? - const firstName = 'Wonderful'; - const lastName = 'Person'; - const userIdentifier = firstName - ? `${firstName} ${lastName}` - : ensName - ? ensName - : userAddress - ? formatAddress(userAddress) - : '0x'; - - const formattedAmount: string = new Decimal(ethers.utils.formatEther(rawAmount) ?? 0).toString(); - const formattedFee: string = new Decimal(ethers.utils.formatEther(fee) ?? 0).toString(); +function TransactionListItem({ + userIdentifier, + isDonation, + amount, + amountIsFormatted, + txHash, + rawNetworkFee, +}: TransactionListItemProps) { + const formattedAmount: string = amountIsFormatted ? amount : new Decimal(ethers.utils.formatEther(amount)).toString(); + const formattedFee: string = new Decimal(ethers.utils.formatEther(rawNetworkFee ?? 0)).toString(); + const formattedHash = txHash.slice(0, 40) + '...'; return ( - {donation ? ( + {isDonation ? ( ) : ( )} - {donation ? ( + {isDonation ? ( ) : ( @@ -55,7 +47,7 @@ function TransactionListItem({ collective, transaction }: TransactionListItemPro - {hash} + {formattedHash} diff --git a/packages/app/src/components/ViewCollective.tsx b/packages/app/src/components/ViewCollective.tsx index 5b6f8e6c..1d418831 100644 --- a/packages/app/src/components/ViewCollective.tsx +++ b/packages/app/src/components/ViewCollective.tsx @@ -6,8 +6,7 @@ import StewardList from './StewardsList/StewardsList'; import TransactionList from './TransactionList/TransactionList'; import { InterSemiBold, InterSmall } from '../utils/webFonts'; import useCrossNavigate from '../routes/useCrossNavigate'; -import StopDonationModal from './StopDonationModal'; -import ThankYouModal from './ThankYouModal'; +import StopDonationModal from './modals/StopDonationModal'; import { Colors } from '../utils/colors'; import { Link, useMediaQuery } from 'native-base'; import { formatTime } from '../lib/formatTime'; @@ -33,7 +32,7 @@ import { import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts'; import FlowingDonationsRowItem from './FlowingDonationsRowItem'; import { useDeleteFlow } from '../hooks/useContractCalls/useDeleteFlow'; -import ErrorModal from './ErrorModal'; +import ErrorModal from './modals/ErrorModal'; import FlowingCurrentPoolRowItem from './FlowingCurrentPoolRowItem'; interface ViewCollectiveProps { @@ -69,7 +68,6 @@ function ViewCollective({ collective }: ViewCollectiveProps) { const isDonating = maybeDonorCollective && maybeDonorCollective.flowRate !== '0'; const [stopDonationModalVisible, setStopDonationModalVisible] = useState(false); - const [donationModalVisible, setDonationModalVisible] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); const handleStopDonation = useDeleteFlow( @@ -229,6 +227,11 @@ function ViewCollective({ collective }: ViewCollectiveProps) { + setErrorMessage(undefined)} + message={errorMessage ?? ''} + /> ); @@ -361,7 +364,6 @@ function ViewCollective({ collective }: ViewCollectiveProps) { message={errorMessage ?? ''} /> - ); diff --git a/packages/app/src/components/WalletDetails/EmptyProfile.tsx b/packages/app/src/components/WalletDetails/EmptyProfile.tsx index eb8f9fc8..59f53cf2 100644 --- a/packages/app/src/components/WalletDetails/EmptyProfile.tsx +++ b/packages/app/src/components/WalletDetails/EmptyProfile.tsx @@ -38,7 +38,7 @@ const styles = StyleSheet.create({ ...InterSmall, }, image: { - height: 169, + height: 154, width: 230, alignSelf: 'center', }, diff --git a/packages/app/src/components/ApproveSwapModal.tsx b/packages/app/src/components/modals/ApproveSwapModal.tsx similarity index 63% rename from packages/app/src/components/ApproveSwapModal.tsx rename to packages/app/src/components/modals/ApproveSwapModal.tsx index a10e2640..417f094e 100644 --- a/packages/app/src/components/ApproveSwapModal.tsx +++ b/packages/app/src/components/modals/ApproveSwapModal.tsx @@ -1,8 +1,8 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -import { Colors } from '../utils/colors'; -import { modalStyles } from './shared'; -import { CloseIcon, ApproveTokenImg } from '../assets'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; +import { modalStyles } from '../shared'; +import { CloseIcon, ApproveTokenImg } from '../../assets'; interface AproveSwapModalProps { openModal: boolean; @@ -11,25 +11,23 @@ interface AproveSwapModalProps { const ApproveSwapModal = ({ openModal, setOpenModal }: AproveSwapModalProps) => { return ( - - - - - - setOpenModal(false)}> - - - - APPROVE TOKEN SWAP - - To approve the exchange from your donation currency to this GoodCollective's currency, sign with your - wallet. - - woman + + + + + setOpenModal(false)}> + + + APPROVE TOKEN SWAP + + To approve the exchange from your donation currency to this GoodCollective's currency, sign with your + wallet. + + woman - - + + ); }; diff --git a/packages/app/src/components/CompleteDonationModal.tsx b/packages/app/src/components/modals/CompleteDonationModal.tsx similarity index 62% rename from packages/app/src/components/CompleteDonationModal.tsx rename to packages/app/src/components/modals/CompleteDonationModal.tsx index 0dac6725..56d6051e 100644 --- a/packages/app/src/components/CompleteDonationModal.tsx +++ b/packages/app/src/components/modals/CompleteDonationModal.tsx @@ -1,9 +1,8 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -// import useCrossNavigate from '../routes/useCrossNavigate'; -import { Colors } from '../utils/colors'; -import { modalStyles } from './shared'; -import { CloseIcon, PhoneImg } from '../assets'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; +import { modalStyles } from '../shared'; +import { CloseIcon, PhoneImg } from '../../assets'; interface CompleteDonationModalProps { openModal: boolean; @@ -14,25 +13,20 @@ const CompleteDonationModal = ({ openModal, setOpenModal }: CompleteDonationModa const onClickClose = () => setOpenModal(false); return ( - - - - - - - - - - COMPLETE YOUR DONATION - To complete your donation, sign with your wallet. - woman - - OK + + + + + + + COMPLETE YOUR DONATION + To complete your donation, sign with your wallet. + woman - - + + ); }; diff --git a/packages/app/src/components/ErrorModal.tsx b/packages/app/src/components/modals/ErrorModal.tsx similarity index 65% rename from packages/app/src/components/ErrorModal.tsx rename to packages/app/src/components/modals/ErrorModal.tsx index 1a132d9e..8e6e3b72 100644 --- a/packages/app/src/components/ErrorModal.tsx +++ b/packages/app/src/components/modals/ErrorModal.tsx @@ -1,7 +1,7 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -import { Colors } from '../utils/colors'; -import { CloseIcon, ThankYouImg } from '../assets'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; +import { CloseIcon, ThankYouImg } from '../../assets'; interface ErrorModalProps { openModal: boolean; @@ -12,27 +12,25 @@ interface ErrorModalProps { const ErrorModal = ({ openModal, setOpenModal, message }: ErrorModalProps) => { const onCloseClicked = () => setOpenModal(false); return ( - - - - - - - - - - - SOMETHING WENT WRONG - Please try again later. - Reason: {message} - woman - - OK + + + + + + + + SOMETHING WENT WRONG + Please try again later. + Reason: {message} + woman + + OK + - - + + ); }; diff --git a/packages/app/src/components/StopDonationModal.tsx b/packages/app/src/components/modals/StopDonationModal.tsx similarity index 63% rename from packages/app/src/components/StopDonationModal.tsx rename to packages/app/src/components/modals/StopDonationModal.tsx index af646dec..c6c45c40 100644 --- a/packages/app/src/components/StopDonationModal.tsx +++ b/packages/app/src/components/modals/StopDonationModal.tsx @@ -1,8 +1,7 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -// import useCrossNavigate from '../routes/useCrossNavigate'; -import { Colors } from '../utils/colors'; -import { QuestionImg } from '../assets'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; +import { QuestionImg } from '../../assets'; interface StopDonationModalProps { openModal: boolean; @@ -10,26 +9,22 @@ interface StopDonationModalProps { } const StopDonationModal = ({ openModal, setOpenModal }: StopDonationModalProps) => { - // const { navigate } = useCrossNavigate(); return ( - - - - - ARE YOU SURE YOU WANT TO STOP YOUR DONATION? - - If so, please sign with your wallet. If not, please click below to return to the GoodCollective you - support. - + + + + ARE YOU SURE YOU WANT TO STOP YOUR DONATION? + + If so, please sign with your wallet. If not, please click below to return to the GoodCollective you support. + - woman - setOpenModal(false)}> - GO BACK - - + woman + setOpenModal(false)}> + GO BACK + - - + + ); }; diff --git a/packages/app/src/components/SwitchModal.tsx b/packages/app/src/components/modals/SwitchModal.tsx similarity index 98% rename from packages/app/src/components/SwitchModal.tsx rename to packages/app/src/components/modals/SwitchModal.tsx index 0a40c9b9..281947f7 100644 --- a/packages/app/src/components/SwitchModal.tsx +++ b/packages/app/src/components/modals/SwitchModal.tsx @@ -1,7 +1,6 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -// import useCrossNavigate from '../routes/useCrossNavigate'; -import { Colors } from '../utils/colors'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; const WomanUri = `data:image/svg+xml;utf8, `; @@ -11,28 +10,25 @@ interface SwitchModalProps { } const SwitchModal = ({ openModal, setOpenModal }: SwitchModalProps) => { - // const { navigate } = useCrossNavigate(); return ( - - - - - PLEASE USE CELO - - GoodCollective only supports the CELO network at this time. Please use the CELO network. - - woman - { - setOpenModal(false); - }}> - OK - - + + + + PLEASE USE CELO + + GoodCollective only supports the CELO network at this time. Please use the CELO network. + + woman + { + setOpenModal(false); + }}> + OK + - - + + ); }; diff --git a/packages/app/src/components/ThankYouModal.tsx b/packages/app/src/components/modals/ThankYouModal.tsx similarity index 55% rename from packages/app/src/components/ThankYouModal.tsx rename to packages/app/src/components/modals/ThankYouModal.tsx index e9273110..5f6950f4 100644 --- a/packages/app/src/components/ThankYouModal.tsx +++ b/packages/app/src/components/modals/ThankYouModal.tsx @@ -1,37 +1,34 @@ import { Modal, StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native'; -import useCrossNavigate from '../routes/useCrossNavigate'; -import { InterRegular, InterSemiBold } from '../utils/webFonts'; -import { Colors } from '../utils/colors'; -import { useAccount } from 'wagmi'; -import { ThankYouImg } from '../assets'; +import { InterRegular, InterSemiBold } from '../../utils/webFonts'; +import { Colors } from '../../utils/colors'; +import { ThankYouImg } from '../../assets'; +import { IpfsCollective } from '../../models/models'; interface ThankYouModalProps { openModal: boolean; - setOpenModal: any; + setOpenModal: (openModal: boolean) => void; + collective: IpfsCollective; } -const ThankYouModal = ({ openModal }: ThankYouModalProps) => { - // const [modalVisible, setModalVisible] = useState(openModal); - const { navigate } = useCrossNavigate(); - const { address } = useAccount(); +const ThankYouModal = ({ openModal, setOpenModal, collective }: ThankYouModalProps) => { + const onClick = () => setOpenModal(false); + return ( - - - - - THANK YOU! - You have just donated to Restoring the Kakamega Forest GoodCollective! - - To stop your donation, visit the Restoring the Kakamega Forest GoodCollective page. - - woman - navigate('/walletProfile/' + address)}> - GO TO PROFILE - - + + + + THANK YOU! + {`You have just donated to ${collective.name} GoodCollective!`} + + {`To stop your donation, visit the ${collective.name} GoodCollective page.`} + + woman + + GO TO PROFILE + - - + + ); }; diff --git a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx index 4b9a6146..f4ac5e6a 100644 --- a/packages/app/src/hooks/useContractCalls/useContractCalls.tsx +++ b/packages/app/src/hooks/useContractCalls/useContractCalls.tsx @@ -20,6 +20,7 @@ export const useContractCalls = ( frequency: Frequency, onError: (error: string) => void, toggleCompleteDonationModal: (value: boolean) => void, + toggleThankYouModal: (value: boolean) => void, minReturnFromSwap?: string, swapPath?: string ): ContractCalls => { @@ -32,7 +33,8 @@ export const useContractCalls = ( duration, frequency, onError, - toggleCompleteDonationModal + toggleCompleteDonationModal, + toggleThankYouModal ); const supportFlowWithSwap = useSupportFlowWithSwap( collective, @@ -42,6 +44,7 @@ export const useContractCalls = ( frequency, onError, toggleCompleteDonationModal, + toggleThankYouModal, minReturnFromSwap, swapPath ); @@ -50,14 +53,16 @@ export const useContractCalls = ( tokenIn.decimals, decimalAmountIn, onError, - toggleCompleteDonationModal + toggleCompleteDonationModal, + toggleThankYouModal ); const supportSingleBatch = useSupportSingleBatch( collective, tokenIn.decimals, decimalAmountIn, onError, - toggleCompleteDonationModal + toggleCompleteDonationModal, + toggleThankYouModal ); return { diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts index 97cb638c..37d7ea9a 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlow.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlow.ts @@ -14,7 +14,8 @@ export function useSupportFlow( duration: number, frequency: Frequency, onError: (error: string) => void, - toggleCompleteDonationModal: (value: boolean) => void + toggleCompleteDonationModal: (value: boolean) => void, + toggleThankYouModal: (value: boolean) => void ) { const { address: maybeAddress } = useAccount(); const { chain } = useNetwork(); @@ -42,25 +43,29 @@ export function useSupportFlow( const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); toggleCompleteDonationModal(true); const tx = await sdk.supportFlow(signer, collective, flowRate); + toggleCompleteDonationModal(false); + toggleThankYouModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { toggleCompleteDonationModal(false); + toggleThankYouModal(false); const message = printAndParseSupportError(error); onError(message); } }, [ maybeAddress, chain?.id, - collective, - currencyDecimals, + maybeSigner, decimalAmountIn, duration, frequency, - navigate, + currencyDecimals, onError, - maybeSigner, toggleCompleteDonationModal, + collective, + toggleThankYouModal, + navigate, ]); } diff --git a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts index 550c3080..9d168a37 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportFlowWithSwap.ts @@ -18,6 +18,7 @@ export function useSupportFlowWithSwap( frequency: Frequency, onError: (error: string) => void, toggleCompleteDonationModal: (value: boolean) => void, + toggleThankYouModal: (value: boolean) => void, minReturnFromSwap?: string, swapPath?: string ) { @@ -64,27 +65,32 @@ export function useSupportFlowWithSwap( swapFrom: tokenIn.address, deadline: Math.floor(Date.now() / 1000 + 1800).toString(), }); + toggleCompleteDonationModal(false); + toggleThankYouModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { toggleCompleteDonationModal(false); + toggleThankYouModal(false); const message = printAndParseSupportError(error); onError(message); } }, [ maybeAddress, chain?.id, - collective, - tokenIn, + maybeSigner, + minReturnFromSwap, + swapPath, decimalAmountIn, duration, frequency, - navigate, + tokenIn.decimals, + tokenIn.address, onError, - maybeSigner, - minReturnFromSwap, - swapPath, toggleCompleteDonationModal, + collective, + toggleThankYouModal, + navigate, ]); } diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts index d725a052..d4fbc170 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleBatch.ts @@ -13,7 +13,8 @@ export function useSupportSingleBatch( currencyDecimals: number, decimalAmountIn: number, onError: (error: string) => void, - toggleCompleteDonationModal: (value: boolean) => void + toggleCompleteDonationModal: (value: boolean) => void, + toggleThankYouModal: (value: boolean) => void ) { const { address: maybeAddress } = useAccount(); const { chain } = useNetwork(); @@ -40,23 +41,27 @@ export function useSupportSingleBatch( const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); toggleCompleteDonationModal(true); const tx = await sdk.supportSingleBatch(signer, collective, donationAmount); + toggleCompleteDonationModal(false); + toggleThankYouModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { toggleCompleteDonationModal(false); + toggleThankYouModal(false); const message = printAndParseSupportError(error); onError(message); } }, [ maybeAddress, chain?.id, - collective, - currencyDecimals, + maybeSigner, decimalAmountIn, - navigate, + currencyDecimals, onError, - maybeSigner, toggleCompleteDonationModal, + collective, + toggleThankYouModal, + navigate, ]); } diff --git a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts index 9380ab39..261df1b5 100644 --- a/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts +++ b/packages/app/src/hooks/useContractCalls/useSupportSingleTransferAndCall.ts @@ -13,7 +13,8 @@ export function useSupportSingleTransferAndCall( currencyDecimals: number, decimalAmountIn: number, onError: (error: string) => void, - toggleCompleteDonationModal: (value: boolean) => void + toggleCompleteDonationModal: (value: boolean) => void, + toggleThankYouModal: (value: boolean) => void ) { const { address: maybeAddress } = useAccount(); const { chain } = useNetwork(); @@ -40,23 +41,27 @@ export function useSupportSingleTransferAndCall( const sdk = new GoodCollectiveSDK(chainIdString, signer.provider, { network }); toggleCompleteDonationModal(true); const tx = await sdk.supportSingleTransferAndCall(signer, collective, donationAmount); + toggleCompleteDonationModal(false); + toggleThankYouModal(true); await tx.wait(); navigate(`/profile/${address}`); return; } catch (error) { toggleCompleteDonationModal(false); + toggleThankYouModal(false); const message = printAndParseSupportError(error); onError(message); } }, [ maybeAddress, chain?.id, - collective, - currencyDecimals, + maybeSigner, decimalAmountIn, - navigate, + currencyDecimals, onError, - maybeSigner, toggleCompleteDonationModal, + collective, + toggleThankYouModal, + navigate, ]); } diff --git a/packages/app/src/hooks/useRecentTransactions.ts b/packages/app/src/hooks/useRecentTransactions.ts index 62ceddf8..d23dbe7f 100644 --- a/packages/app/src/hooks/useRecentTransactions.ts +++ b/packages/app/src/hooks/useRecentTransactions.ts @@ -1,14 +1,37 @@ -import { Transaction } from '../models/models'; +import { ClaimTx, SupportTx, Transaction } from '../models/models'; +import { useSubgraphClaimsByCollectiveId, useSubgraphSupportEventsByCollectiveId } from '../subgraph'; +import { subgraphClaimToModel, subgraphSupportEventToModel } from '../models/transforms'; +import { useMemo } from 'react'; -export const useRecentTransactions = (collective: `0x${string}`, maxN: number): Transaction[] => { - return Array(maxN) - .fill(0) - .map((e, i) => ({ - hash: '0x123' + i.toString(), - rawAmount: '500000000000000000', - from: '0x123', - to: collective, - fee: '4000000000000000', - timestamp: 1, - })); +export function useRecentClaims(collective: `0x${string}`, maxN: number, pollInterval?: number): ClaimTx[] | undefined { + const subgraphClaims = useSubgraphClaimsByCollectiveId(collective, maxN, pollInterval); + if (!subgraphClaims) return undefined; + return subgraphClaims.map(subgraphClaimToModel); +} + +export function useRecentDonations( + collective: `0x${string}`, + maxN: number, + pollInterval?: number +): SupportTx[] | undefined { + const subgraphSupportEvents = useSubgraphSupportEventsByCollectiveId(collective, maxN, pollInterval); + if (!subgraphSupportEvents) return undefined; + return subgraphSupportEvents.map(subgraphSupportEventToModel); +} + +export const useRecentTransactions = ( + collective: `0x${string}`, + maxN: number, + donationsPollInterval?: number, + claimsPollInterval?: number +): Transaction[] => { + const claims: Transaction[] | undefined = useRecentClaims(collective, maxN, claimsPollInterval); + const donations: Transaction[] | undefined = useRecentDonations(collective, maxN, donationsPollInterval); + return useMemo(() => { + const transactions: Transaction[] = [...(claims ?? []), ...(donations ?? [])]; + transactions.sort((a, b) => { + return -1 * (a.timestamp - b.timestamp); + }); + return transactions.slice(0, maxN); + }, [claims, donations, maxN]); }; diff --git a/packages/app/src/models/models.ts b/packages/app/src/models/models.ts index 9c2294f7..39e78f24 100644 --- a/packages/app/src/models/models.ts +++ b/packages/app/src/models/models.ts @@ -63,9 +63,21 @@ export interface IpfsCollective { export interface Transaction { hash: string; - rawAmount: string; - from: string; - to: string; - fee: string; + networkFee: string; + collective: string; timestamp: number; } + +export interface ClaimTx extends Transaction { + stewards: string[]; + totalRewards: string; +} + +export interface SupportTx extends Transaction { + donor: string; + contribution: string; + previousContribution: string; + isFlowUpdate: boolean; + flowRate: string; + previousFlowRate: string; +} diff --git a/packages/app/src/models/transforms.ts b/packages/app/src/models/transforms.ts index 1c513e6b..de8d32c8 100644 --- a/packages/app/src/models/transforms.ts +++ b/packages/app/src/models/transforms.ts @@ -1,11 +1,22 @@ -import { Collective, Donor, Steward, IpfsCollective, StewardCollective, DonorCollective } from './models'; import { + Collective, + Donor, + Steward, + IpfsCollective, + StewardCollective, + DonorCollective, + ClaimTx, + SupportTx, +} from './models'; +import { + SubgraphClaim, SubgraphCollective, SubgraphDonor, SubgraphDonorCollective, SubgraphIpfsCollective, SubgraphSteward, SubgraphStewardCollective, + SubgraphSupportEvent, } from '../subgraph'; export function subgraphStewardCollectiveToModel( @@ -34,14 +45,14 @@ export function subgraphDonorCollectiveToModel(subgraphDonorCollective: Subgraph collective: subgraphDonorCollective.collective.id, contribution: subgraphDonorCollective.contribution, flowRate: subgraphDonorCollective.flowRate, - timestamp: parseInt(subgraphDonorCollective.timestamp, 10), + timestamp: subgraphDonorCollective.timestamp, }; } export function subgraphDonorToModel(subgraphDonor: SubgraphDonor): Donor { return { address: subgraphDonor.id, - joined: parseInt(subgraphDonor.joined, 10), + joined: subgraphDonor.timestamp, totalDonated: subgraphDonor.totalDonated, collectives: subgraphDonor.collectives.map(subgraphDonorCollectiveToModel), }; @@ -80,3 +91,30 @@ export function ipfsSubgraphCollectiveToModel(subgraphCollective: { images: subgraphCollective.ipfs?.images, }; } + +export function subgraphClaimToModel(subgraphClaim: SubgraphClaim): ClaimTx { + const stewards = subgraphClaim.events.flatMap((event) => event.contributors.map((contributor) => contributor.id)); + return { + hash: subgraphClaim.txHash, + networkFee: subgraphClaim.networkFee, + collective: subgraphClaim.collective.id, + timestamp: subgraphClaim.timestamp, + stewards: stewards, + totalRewards: subgraphClaim.totalRewards, + }; +} + +export function subgraphSupportEventToModel(subgraphSupportEvent: SubgraphSupportEvent): SupportTx { + return { + hash: subgraphSupportEvent.id, + networkFee: subgraphSupportEvent.networkFee, + collective: subgraphSupportEvent.collective.id, + timestamp: subgraphSupportEvent.timestamp, + donor: subgraphSupportEvent.donor.id, + contribution: subgraphSupportEvent.contribution, + previousContribution: subgraphSupportEvent.previousContribution, + isFlowUpdate: subgraphSupportEvent.isFlowUpdate, + flowRate: subgraphSupportEvent.flowRate, + previousFlowRate: subgraphSupportEvent.previousFlowRate, + }; +} diff --git a/packages/app/src/models/typeUtil.ts b/packages/app/src/models/typeUtil.ts new file mode 100644 index 00000000..f0bde68d --- /dev/null +++ b/packages/app/src/models/typeUtil.ts @@ -0,0 +1,9 @@ +import { ClaimTx, SupportTx, Transaction } from './models'; + +export function isSupportTx(transaction: Transaction): transaction is SupportTx { + return (transaction as SupportTx).donor !== undefined; +} + +export function isClaimTx(transaction: Transaction): transaction is ClaimTx { + return (transaction as ClaimTx).stewards !== undefined; +} diff --git a/packages/app/src/pages/ModalTestPage.tsx b/packages/app/src/pages/ModalTestPage.tsx deleted file mode 100644 index 074b6659..00000000 --- a/packages/app/src/pages/ModalTestPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Layout from '../components/Layout'; -import SwitchModal from '../components/SwitchModal'; -import CompleteDonationModal from '../components/CompleteDonationModal'; -import ThankYouModal from '../components/ThankYouModal'; -import ErrorModal from '../components/ErrorModal'; -import ApproveSwapModal from '../components/ApproveSwapModal'; -import StopDonationModal from '../components/StopDonationModal'; -import { useState } from 'react'; - -function ModalTestPage() { - const [openModal, setOpenModal] = useState(false); - return ( - - - - - - - - - ); -} - -export default ModalTestPage; diff --git a/packages/app/src/pages/WalletProfilePage.tsx b/packages/app/src/pages/WalletProfilePage.tsx index a7896867..cf1fff01 100644 --- a/packages/app/src/pages/WalletProfilePage.tsx +++ b/packages/app/src/pages/WalletProfilePage.tsx @@ -10,7 +10,7 @@ import { useEnsName } from 'wagmi'; function WalletProfilePage() { const location = useLocation(); const profileAddress = location.pathname.slice('/profile/'.length).toLocaleLowerCase(); - const donor = useDonorById(profileAddress, 500); + const donor = useDonorById(profileAddress, 1000); const steward = useStewardById(profileAddress); const [isDesktopResolution] = useMediaQuery({ diff --git a/packages/app/src/subgraph/index.ts b/packages/app/src/subgraph/index.ts index c0b0c129..4f3c6373 100644 --- a/packages/app/src/subgraph/index.ts +++ b/packages/app/src/subgraph/index.ts @@ -4,3 +4,5 @@ export * from './useSubgraphDonorCollective'; export * from './useSubgraphSteward'; export * from './useSubgraphCollective'; export * from './useSubgraphIpfsCollective'; +export * from './useSubgraphClaim'; +export * from './useSubgraphSupportEvent'; diff --git a/packages/app/src/subgraph/subgraphModels.ts b/packages/app/src/subgraph/subgraphModels.ts index 3e08c46c..0d41a83e 100644 --- a/packages/app/src/subgraph/subgraphModels.ts +++ b/packages/app/src/subgraph/subgraphModels.ts @@ -1,6 +1,6 @@ export type SubgraphDonor = { id: string; - joined: string; + timestamp: number; totalDonated: string; collectives: SubgraphDonorCollective[]; }; @@ -11,7 +11,8 @@ export type SubgraphDonorCollective = { collective: SubgraphCollective | { id: string }; contribution: string; flowRate: string; - timestamp: string; + timestamp: number; + events?: SubgraphSupportEvent[]; }; export type SubgraphSteward = { @@ -33,8 +34,8 @@ export type SubgraphStewardCollective = { export type SubgraphCollective = { id: string; ipfs: SubgraphIpfsCollective; - settings?: PoolSettings; - limits?: SafetyLimits; + settings?: SubgraphPoolSettings; + limits?: SubgraphSafetyLimits; donors?: SubgraphDonorCollective[]; stewards?: SubgraphStewardCollective[]; projectId?: string; @@ -44,6 +45,7 @@ export type SubgraphCollective = { paymentsMade: number; totalDonations: string; totalRewards: string; + claims?: SubgraphClaim[]; }; export type SubgraphIpfsCollective = { @@ -61,7 +63,7 @@ export type SubgraphIpfsCollective = { images?: string[]; }; -export type PoolSettings = { +export type SubgraphPoolSettings = { id: string; nftType: string; manager: string; @@ -70,7 +72,7 @@ export type PoolSettings = { rewardToken: string; }; -export type SafetyLimits = { +export type SubgraphSafetyLimits = { id: string; maxTotalPerMonth: string; maxMemberPerMonth: string; @@ -81,24 +83,41 @@ export type SubgraphProvableNFT = { id: string; owner: string; hash: string; - steward: SubgraphSteward[] | string[]; + steward: SubgraphSteward[] | { id: string }[]; collective: SubgraphCollective | string; }; -export type EventData = { - id: string; +export type SubgraphClaimEvent = { + id: string; // event uri eventType: number; - timestamp: string; + timestamp: number; quantity: string; - uri: string; rewardPerContributor: string; contributors: SubgraphSteward[] | { id: string }[]; nft: SubgraphProvableNFT | { id: string }; - claim: Claim; + claim: SubgraphClaim | { id: string }; }; -export type Claim = { +export type SubgraphClaim = { id: string; + collective: SubgraphCollective | { id: string }; + txHash: string; + networkFee: string; totalRewards: string; - event: EventData | { id: string }; + events: SubgraphClaimEvent[]; + timestamp: number; +}; + +export type SubgraphSupportEvent = { + id: string; // tx hash + networkFee: string; + donor: SubgraphDonor | { id: string }; + collective: SubgraphCollective | { id: string }; + donorCollective: SubgraphDonorCollective | { id: string }; + contribution: string; + previousContribution: string; + isFlowUpdate: boolean; + flowRate: string; + previousFlowRate: string; + timestamp: number; }; diff --git a/packages/app/src/subgraph/useSubgraphClaim.ts b/packages/app/src/subgraph/useSubgraphClaim.ts new file mode 100644 index 00000000..53d80350 --- /dev/null +++ b/packages/app/src/subgraph/useSubgraphClaim.ts @@ -0,0 +1,53 @@ +import { gql } from '@apollo/client'; +import { SubgraphClaim } from './subgraphModels'; +import { ClaimsSubgraphResponse, useSubgraphData } from './useSubgraphData'; + +const claimsByCollective = gql` + query CLAIMS_BY_COLLECTIVE($collective: String, $n: Int!) { + claims(where: { collective: $collective }, orderBy: timestamp, orderDirection: desc, first: $n) { + id + collective { + id + } + txHash + networkFee + totalRewards + timestamp + events { + id + eventType + timestamp + quantity + rewardPerContributor + contributors { + id + } + nft { + id + } + claim { + id + } + } + } + } +`; + +export function useSubgraphClaimsByCollectiveId( + collectiveAddress: string, + n: number, + pollInterval?: number +): SubgraphClaim[] | undefined { + const response = useSubgraphData(claimsByCollective, { + variables: { + collective: collectiveAddress, + n, + }, + pollInterval, + }); + const data = (response as ClaimsSubgraphResponse).claims; + if (!data || data.length === 0) { + return undefined; + } + return data; +} diff --git a/packages/app/src/subgraph/useSubgraphCollective.ts b/packages/app/src/subgraph/useSubgraphCollective.ts index bea45075..987bbc40 100644 --- a/packages/app/src/subgraph/useSubgraphCollective.ts +++ b/packages/app/src/subgraph/useSubgraphCollective.ts @@ -50,7 +50,7 @@ export const collective = gql` `; export const collectivesById = gql` - query COLLECTIVES_BY_ID($ids: [String]) { + query COLLECTIVES_BY_ID($ids: [String!]) { collectives(where: { id_in: $ids }) { id ipfs { diff --git a/packages/app/src/subgraph/useSubgraphData.ts b/packages/app/src/subgraph/useSubgraphData.ts index 8b137743..3ac2706d 100644 --- a/packages/app/src/subgraph/useSubgraphData.ts +++ b/packages/app/src/subgraph/useSubgraphData.ts @@ -1,11 +1,13 @@ import { LazyQueryHookOptions, OperationVariables, TypedDocumentNode, useQuery } from '@apollo/client'; import { DocumentNode } from 'graphql/language'; import { + SubgraphClaim, SubgraphCollective, SubgraphDonor, SubgraphDonorCollective, SubgraphIpfsCollective, SubgraphSteward, + SubgraphSupportEvent, } from './subgraphModels'; export type IpfsCollectivesSubgraphResponse = { collectives?: { id: string; ipfs: SubgraphIpfsCollective }[] }; @@ -13,6 +15,8 @@ export type CollectivesSubgraphResponse = { collectives?: SubgraphCollective[] } export type DonorsSubgraphResponse = { donors?: SubgraphDonor[] }; export type StewardsSubgraphResponse = { stewards?: SubgraphSteward[] }; export type DonorCollectiveSubgraphResponse = { donorCollectives?: SubgraphDonorCollective[] }; +export type ClaimsSubgraphResponse = { claims?: SubgraphClaim[] }; +export type SupportEventsSubgraphResponse = { supportEvents?: SubgraphSupportEvent[] }; export function useSubgraphData( query: DocumentNode | TypedDocumentNode, diff --git a/packages/app/src/subgraph/useSubgraphDonor.ts b/packages/app/src/subgraph/useSubgraphDonor.ts index 43dbee20..db0a199f 100644 --- a/packages/app/src/subgraph/useSubgraphDonor.ts +++ b/packages/app/src/subgraph/useSubgraphDonor.ts @@ -6,7 +6,7 @@ const donor = gql` query DONOR($id: String) { donors(where: { id: $id }) { id - joined + timestamp totalDonated collectives { id diff --git a/packages/app/src/subgraph/useSubgraphIpfsCollective.ts b/packages/app/src/subgraph/useSubgraphIpfsCollective.ts index d2b4cab2..1737fc58 100644 --- a/packages/app/src/subgraph/useSubgraphIpfsCollective.ts +++ b/packages/app/src/subgraph/useSubgraphIpfsCollective.ts @@ -23,7 +23,7 @@ const allIpfsCollectives = gql` `; const ipfsCollectivesById = gql` - query IPFS_COLLECTIVES_BY_ID($ids: [String]) { + query IPFS_COLLECTIVES_BY_ID($ids: [String!]) { collectives(where: { id_in: $ids }) { id ipfs { diff --git a/packages/app/src/subgraph/useSubgraphSupportEvent.ts b/packages/app/src/subgraph/useSubgraphSupportEvent.ts new file mode 100644 index 00000000..50e6add0 --- /dev/null +++ b/packages/app/src/subgraph/useSubgraphSupportEvent.ts @@ -0,0 +1,46 @@ +import { gql } from '@apollo/client'; +import { SubgraphSupportEvent } from './subgraphModels'; +import { SupportEventsSubgraphResponse, useSubgraphData } from './useSubgraphData'; + +const supportEventsByCollective = gql` + query SUPPORT_EVENTS_BY_COLLECTIVE($collective: String, $n: Int!) { + supportEvents(where: { collective: $collective }, orderBy: timestamp, orderDirection: desc, first: $n) { + id + networkFee + donor { + id + } + collective { + id + } + donorCollective { + id + } + contribution + previousContribution + isFlowUpdate + flowRate + previousFlowRate + timestamp + } + } +`; + +export function useSubgraphSupportEventsByCollectiveId( + collectiveAddress: string, + n: number, + pollInterval?: number +): SubgraphSupportEvent[] | undefined { + const response = useSubgraphData(supportEventsByCollective, { + variables: { + collective: collectiveAddress, + n, + }, + pollInterval, + }); + const data = (response as SupportEventsSubgraphResponse).supportEvents; + if (!data || data.length === 0) { + return undefined; + } + return data; +} diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 054cd76a..d1ddd16b 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -1,6 +1,6 @@ type Donor @entity { id: String! # The address of the donor - joined: BigInt! + timestamp: Int! totalDonated: BigInt! collectives: [DonorCollective!]! @derivedFrom(field: "donor") } @@ -12,9 +12,24 @@ type DonorCollective @entity { id: String! # donorAddress + " " + collectiveAddress donor: Donor! collective: Collective! - contribution: BigInt! # This is the total contribution of the donor to the collective + contribution: BigInt! # total contribution of the donor to the collective flowRate: BigInt! - timestamp: BigInt! + timestamp: Int! + events: [SupportEvent!]! @derivedFrom(field: "donorCollective") +} + +type SupportEvent @entity { + id: String! # tx hash + networkFee: BigInt! # gas price * gas limit + donor: Donor! + collective: Collective! + donorCollective: DonorCollective! + contribution: BigInt! + previousContribution: BigInt! + isFlowUpdate: Boolean! + flowRate: BigInt! + previousFlowRate: BigInt! + timestamp: Int! } type Steward @entity { @@ -58,10 +73,11 @@ type Collective @entity { projectId: String! isVerified: Boolean! poolFactory: String! # pool factory (event source) - timestamp: BigInt! + timestamp: Int! paymentsMade: Int! totalDonations: BigInt! totalRewards: BigInt! + claims: [Claim!]! @derivedFrom(field: "collective") } type IpfsCollective @entity { @@ -103,10 +119,10 @@ type ProvableNFT @entity { collective: Collective # This is always populated, but not by the same handler that creates the ProvableNFT } -type EventData @entity { +type ClaimEvent @entity { id: String! # event uri eventType: Int! - timestamp: BigInt! + timestamp: Int! quantity: BigInt! rewardPerContributor: BigInt! contributors: [Steward!]! @@ -116,6 +132,10 @@ type EventData @entity { type Claim @entity { id: String! + collective: Collective! + txHash: String! + networkFee: BigInt! # gas price * gas limit totalRewards: BigInt! - events: [EventData!]! @derivedFrom(field: "claim") + events: [ClaimEvent!]! @derivedFrom(field: "claim") + timestamp: Int! } diff --git a/packages/subgraph/src/mappings/pool.ts b/packages/subgraph/src/mappings/pool.ts index 0db53f0a..b11a4731 100644 --- a/packages/subgraph/src/mappings/pool.ts +++ b/packages/subgraph/src/mappings/pool.ts @@ -8,7 +8,7 @@ import { import { Claim, Collective, - EventData, + ClaimEvent, PoolSettings, ProvableNFT, SafetyLimits, @@ -85,28 +85,28 @@ export function handleRewardClaim(event: EventRewardClaimed): void { return; } - let eventData = EventData.load(eventUri); - if (eventData !== null) { - return; - } - eventData = new EventData(eventUri); - eventData.claim = claimId.toHexString(); - eventData.eventType = eventType; - eventData.timestamp = eventTimestamp; - eventData.quantity = eventQuantity; - eventData.rewardPerContributor = rewardPerContributor; + const claimEvent = new ClaimEvent(eventUri); + claimEvent.claim = claimId.toHexString(); + claimEvent.eventType = eventType; + claimEvent.timestamp = eventTimestamp.toI32(); + claimEvent.quantity = eventQuantity; + claimEvent.rewardPerContributor = rewardPerContributor; // handle claim let claim = Claim.load(claimId.toHexString()); if (claim === null) { claim = new Claim(claimId.toHexString()); claim.totalRewards = BigInt.fromI32(0); + claim.collective = pool.id; + claim.txHash = event.transaction.hash.toHexString(); + claim.timestamp = event.block.timestamp.toI32(); + claim.networkFee = event.transaction.gasLimit.times(event.transaction.gasPrice); } const eventReward = rewardPerContributor.times(eventQuantity).times(BigInt.fromI32(contributors.length)); claim.totalRewards = claim.totalRewards.plus(eventReward); // handle nft -> note that ProvableNFT.hash and ProvableNFT.owner are set by NFT mint event - eventData.nft = nftId; + claimEvent.nft = nftId; let nft = ProvableNFT.load(nftId); if (nft === null) { nft = new ProvableNFT(nftId); @@ -115,13 +115,13 @@ export function handleRewardClaim(event: EventRewardClaimed): void { } nft.collective = poolAddress; - eventData.contributors = new Array(); + claimEvent.contributors = new Array(); for (let i = 0; i < contributors.length; i++) { const stewardAddress = contributors[i].toHexString(); const stewardCollectiveId = `${stewardAddress} ${poolAddress}`; // adds steward to event data - eventData.contributors.push(stewardAddress); + claimEvent.contributors.push(stewardAddress); // update Steward let steward = Steward.load(contributors[i].toHexString()); @@ -160,7 +160,7 @@ export function handleRewardClaim(event: EventRewardClaimed): void { claim.save(); nft.save(); pool.save(); - eventData.save(); + claimEvent.save(); } export function handleClaim(event: NFTClaimed): void { diff --git a/packages/subgraph/src/mappings/poolFactory.ts b/packages/subgraph/src/mappings/poolFactory.ts index 943af3f8..15ee7453 100644 --- a/packages/subgraph/src/mappings/poolFactory.ts +++ b/packages/subgraph/src/mappings/poolFactory.ts @@ -53,7 +53,7 @@ export function handlePoolCreated(event: PoolCreated): void { directPaymentPool.projectId = projectID; directPaymentPool.isVerified = false; directPaymentPool.poolFactory = event.address.toHexString(); - directPaymentPool.timestamp = event.block.timestamp; + directPaymentPool.timestamp = event.block.timestamp.toI32(); directPaymentPool.paymentsMade = 0; directPaymentPool.totalDonations = new BigInt(0); directPaymentPool.totalRewards = new BigInt(0); diff --git a/packages/subgraph/src/mappings/superApp.ts b/packages/subgraph/src/mappings/superApp.ts index 5e82135f..6665b3d7 100644 --- a/packages/subgraph/src/mappings/superApp.ts +++ b/packages/subgraph/src/mappings/superApp.ts @@ -1,6 +1,6 @@ import { BigInt, log } from '@graphprotocol/graph-ts'; import { SupporterUpdated } from '../../generated/templates/DirectPaymentsPool/DirectPaymentsPool'; -import { Collective, Donor, DonorCollective } from '../../generated/schema'; +import { Collective, Donor, DonorCollective, SupportEvent } from '../../generated/schema'; export function handleSupport(event: SupporterUpdated): void { const donorAddress = event.params.supporter.toHexString(); @@ -23,7 +23,7 @@ export function handleSupport(event: SupporterUpdated): void { let donor = Donor.load(donorAddress); if (donor == null) { donor = new Donor(donorAddress); - donor.joined = timestamp; + donor.timestamp = timestamp.toI32(); donor.totalDonated = BigInt.fromI32(0); } donor.totalDonated = donor.totalDonated.plus(contributionDelta); @@ -38,11 +38,25 @@ export function handleSupport(event: SupporterUpdated): void { // This value is updated in _updateSupporter at line 260 of GoodCollectiveSuperApp.sol before the event is emitted donorCollective.contribution = event.params.contribution; donorCollective.flowRate = event.params.flowRate; - donorCollective.timestamp = timestamp; + donorCollective.timestamp = timestamp.toI32(); donorCollective.donor = donor.id; donorCollective.collective = pool.id; + // create event + let supportEvent = new SupportEvent(event.transaction.hash.toHexString()); + supportEvent.networkFee = event.transaction.gasLimit.times(event.transaction.gasPrice); + supportEvent.donor = donor.id; + supportEvent.collective = pool.id; + supportEvent.donorCollective = donorCollective.id; + supportEvent.contribution = event.params.contribution; + supportEvent.previousContribution = event.params.previousContribution; + supportEvent.isFlowUpdate = event.params.isFlowUpdate; + supportEvent.flowRate = event.params.flowRate; + supportEvent.previousFlowRate = event.params.previousFlowRate; + supportEvent.timestamp = timestamp.toI32(); + donor.save(); donorCollective.save(); + supportEvent.save(); pool.save(); }