Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
21f7c69
chore(): implemented fee details on the donate component UI
Jul 24, 2025
5edf7cf
chore(): style property of warning box fixed
Jul 24, 2025
c6d2c83
chore(): added fee section on donor collective card alongside tooltips
Jul 24, 2025
3761f52
feat: integrate collective fees into DonorCollectiveCard component
Jul 28, 2025
44c0baf
fix: improve error handling and fee display in DonorCollectiveCard
Aug 20, 2025
9866d72
fix: enhance error handling and fallback logic in useCollectiveFees hook
Aug 20, 2025
8161c30
Revert "fix: enhance error handling and fallback logic in useCollecti…
Aug 20, 2025
284039e
fix: update supportingLabel style in WalletCards component
Aug 20, 2025
2beb4c5
fix: optimize fee calculation in formatFlowRateToDaily function
Aug 20, 2025
21020dd
fix: improve error handling in fee calculation functions
Aug 20, 2025
d3dac2b
feat: enhance DonateComponent to display dynamic fee information
Aug 20, 2025
1a6fc6e
refactor: remove debug logging from useCollectiveFees and GoodCollect…
Aug 26, 2025
6f788ca
feat: integrate realtime stats into DonorCollectiveCard component
Aug 29, 2025
7219dbb
feat: display accumulated fees in ViewCollective component
Sep 2, 2025
5392e9b
feat: refactor useCollectiveFees to simplify fee retrieval logic
Sep 2, 2025
35359bc
refactor: remove useRealtimeStats from DonorCollectiveCard component
Sep 2, 2025
180f640
refactor: update useCollectiveFees and useRealtimeStats to utilize pr…
Sep 4, 2025
7326e53
Apply suggestions from code review
L03TJ3 Sep 4, 2025
69f99f1
Update packages/app/src/components/DonateComponent.tsx
L03TJ3 Sep 4, 2025
743ab6d
Update packages/app/src/components/DonateComponent.tsx
L03TJ3 Sep 4, 2025
4d4a756
style: adjust zIndex and overflow properties in WalletCards component
Sep 4, 2025
089cab0
Update packages/app/src/components/DonateComponent.tsx
L03TJ3 Sep 4, 2025
6cbee29
Merge branch 'Show-network-and-admin-fees' of https://github.com/Emek…
Sep 4, 2025
b007d93
fix: correct import statement and add fee documentation link in defaults
Sep 4, 2025
c45fc2b
style: remove zIndex from WalletCards styles and update zIndex logic …
Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ActiveStreamCard } from '../ActiveStreamCard';
import { WalletDonatedCard } from './WalletDonatedCard';
import { useState } from 'react';
import { useCollectiveFees } from '../../hooks/useCollectiveFees';
import { useRealtimeStats } from '../../hooks/useRealtimeStats';
import { calculateFeeAmounts, formatFlowRateToDaily } from '../../lib/calculateFeeAmounts';

interface DonorCollectiveCardProps {
Expand Down Expand Up @@ -78,6 +79,7 @@ function DonorCollectiveCard({ ipfsCollective, donorCollective, ensName, tokenPr
};

const { fees, loading: feesLoading, error: feesError } = useCollectiveFees(donorCollective.collective);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient Fee Data Fetching category Performance

Tell me more
What is the issue?

The fees data is fetched individually for each DonorCollectiveCard instance, potentially causing multiple unnecessary network requests if multiple cards are rendered.

Why this matters

This can lead to increased network traffic and slower page load times, especially when displaying multiple collective cards simultaneously.

Suggested change ∙ Feature Preview

Move the fee fetching logic to a parent component and pass the fees data down as props, or implement a caching mechanism in useCollectiveFees hook using React Query or similar state management solution:

const queryClient = useQueryClient();
const { data: fees } = useQuery(['collectiveFees', collective], 
  () => fetchFees(collective),
  {
    staleTime: 5 * 60 * 1000, // Cache for 5 minutes
    cacheTime: 30 * 60 * 1000 // Keep unused data for 30 minutes
  }
);
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.

const { stats: realtimeStats } = useRealtimeStats(donorCollective.collective);

const feeAmounts =
fees && donorCollective.flowRate && Number(donorCollective.flowRate) > 0
Expand Down Expand Up @@ -107,7 +109,8 @@ function DonorCollectiveCard({ ipfsCollective, donorCollective, ensName, tokenPr
: 'Unknown';

// Debug: Show if we're using actual fees or fallback values
const isUsingActualFees = fees && fees.protocolFeeBps !== 500 && fees.managerFeeBps !== 300;
const isUsingActualFees =
Boolean(realtimeStats) || (fees && fees.protocolFeeBps !== 500 && fees.managerFeeBps !== 300);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understand what is happening here.
why use realtimestats here, if all you want to show is per-user fee's amount?

  1. getRealTimestats should be used to show the collective accumulated fees, on the goodcollective page
  2. your calculateFeeAmount per user, is what you are showing here.


return (
<TouchableOpacity
Expand Down
64 changes: 64 additions & 0 deletions packages/app/src/hooks/useRealtimeStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAccount } from 'wagmi';
import { useEthersSigner } from './useEthers';
import { GoodCollectiveSDK } from '@gooddollar/goodcollective-sdk';

export type RealtimeStats = {
netIncome: string;
totalFees: string;
protocolFees: string;
managerFees: string;
incomeFlowRate: string;
feeRate: string;
managerFeeRate: string;
};

export function useRealtimeStats(poolAddress: string) {
const { chain } = useAccount();
const maybeSigner = useEthersSigner({ chainId: chain?.id });

const [stats, setStats] = useState<RealtimeStats | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const networkName = useMemo(() => {
if (!chain?.id) return undefined;
const chainIdString = chain.id.toString();
if (chainIdString === '44787') return 'alfajores';
if (chainIdString === '122') return 'fuse';
if (chainIdString === '42220') {
const isProduction =
typeof window !== 'undefined' &&
(window.location.hostname === 'goodcollective.org' ||
window.location.hostname === 'app.goodcollective.org' ||
process.env.NODE_ENV === 'production');
return isProduction ? 'production-celo' : 'development-celo';
}
return undefined;
}, [chain?.id]);

const fetchStats = useCallback(async () => {
if (!poolAddress || !chain?.id || !maybeSigner?.provider || !networkName) {
return;
}
setLoading(true);
setError(null);
try {
const sdk = new GoodCollectiveSDK(chain.id.toString() as any, maybeSigner.provider, { network: networkName });
const result = await sdk.getRealtimeStats(poolAddress);
setStats(result as RealtimeStats);
} catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to fetch realtime stats';
setError(msg);
setStats(null);
} finally {
setLoading(false);
}
}, [poolAddress, chain?.id, maybeSigner?.provider, networkName]);

useEffect(() => {
fetchStats();
}, [fetchStats]);

return { stats, loading, error, refetch: fetchStats };
}
61 changes: 46 additions & 15 deletions packages/sdk-js/src/goodcollective/goodcollective.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumberish, ContractTransaction, ethers } from 'ethers';
import { BigNumberish, ContractTransaction, ContractInterface, ethers } from 'ethers';

import GoodCollectiveContracts from '@gooddollar/goodcollective-contracts/releases/deployment.json' assert { type: 'json' };
import {
Expand All @@ -8,8 +8,7 @@ import {
UBIPool,
UBIPoolFactory,
} from '@gooddollar/goodcollective-contracts/typechain-types/index.ts';
import UBIPoolJson from '@gooddollar/goodcollective-contracts/artifacts/contracts/UBI/UBIPool.sol/UBIPool.json' assert { type: 'json' };
const UBIPoolAbi = UBIPoolJson.abi;
// removed direct JSON import; use deployed contract ABIs from releases file

import { Framework } from '@superfluid-finance/sdk-core';
import { HelperLibrary } from '@gooddollar/goodcollective-contracts/typechain-types/contracts/GoodCollective/GoodCollectiveSuperApp.ts';
Expand Down Expand Up @@ -99,31 +98,36 @@ export class GoodCollectiveSDK {
// w3Storage: Promise<W3Client.Client | void>;
constructor(chainId: Key, readProvider: ethers.providers.Provider, options: SDKOptions = {}) {
this.chainId = chainId;
this.contracts = (GoodCollectiveContracts[chainId] as Array<any>).find((_) =>
options.network ? _.name === options.network : true
)?.contracts as Contracts;
const deployments = GoodCollectiveContracts[chainId as unknown as keyof typeof GoodCollectiveContracts] as
| Array<{
name: string;
contracts: Contracts;
}>
| undefined;
this.contracts = (deployments || []).find((d) => (options.network ? d.name === options.network : true))
?.contracts as Contracts;

const factory = this.contracts.DirectPaymentsFactory || ({} as any);
const factory: Partial<{ address: string; abi: unknown[] }> = this.contracts.DirectPaymentsFactory || {};

this.factory = new ethers.Contract(
factory.address || ethers.constants.AddressZero,
factory.abi || [],
(factory.abi || []) as unknown as ContractInterface,
readProvider
) as DirectPaymentsFactory;

const ubifactory = this.contracts.UBIPoolFactory;
this.ubifactory =
ubifactory && (new ethers.Contract(ubifactory.address, ubifactory.abi, readProvider) as UBIPoolFactory);

const nftEvents = this.contracts.ProvableNFT?.abi.filter((_) => _.type === 'event') || [];
this.pool = new ethers.Contract(
ethers.constants.AddressZero,
(this.contracts.DirectPaymentsPool?.abi || []).concat(nftEvents as []), //add events of nft so they are parsable
readProvider
) as DirectPaymentsPool;
const nftEvents =
(this.contracts.ProvableNFT?.abi as Array<{ type: string }> | undefined)?.filter((_) => _.type === 'event') || [];
const directPoolAbi = ((this.contracts.DirectPaymentsPool?.abi || []) as unknown as Array<string | object>).concat(
nftEvents as unknown as Array<string | object>
) as unknown as ContractInterface;
this.pool = new ethers.Contract(ethers.constants.AddressZero, directPoolAbi, readProvider) as DirectPaymentsPool;
this.ubipool = new ethers.Contract(
ethers.constants.AddressZero,
this.contracts.UBIPool?.abi || UBIPoolAbi || [],
(this.contracts.UBIPool?.abi || []) as unknown as ContractInterface,
readProvider
) as UBIPool;
// initialize framework
Expand Down Expand Up @@ -596,6 +600,32 @@ export class GoodCollectiveSDK {
return tc.transferAndCall(poolAddress, amount, '0x', { ...CHAIN_OVERRIDES[this.chainId] });
}

/**
* Wraps GoodCollectiveSuperApp.getRealtimeStats to return live pool stats
* @param {string} poolAddress - The address of the pool contract
* @returns {Promise<{netIncome:string,totalFees:string,protocolFees:string,managerFees:string,incomeFlowRate:string,feeRate:string,managerFeeRate:string}>}
*/
async getRealtimeStats(poolAddress: string) {
const pool = this.pool.attach(poolAddress);
const [netIncome, totalFees, protocolFees, managerFees, incomeFlowRate, feeRate, managerFeeRate] =
await pool.getRealtimeStats();

// Normalize to strings to avoid downstream BigNumber typing issues
const bnToString = (v: unknown) =>
typeof v === 'object' && v !== null && (v as { _isBigNumber?: boolean })._isBigNumber
? (v as unknown as { toString: () => string }).toString()
: String(v);
return {
netIncome: bnToString(netIncome),
totalFees: bnToString(totalFees),
protocolFees: bnToString(protocolFees),
managerFees: bnToString(managerFees),
incomeFlowRate: bnToString(incomeFlowRate),
feeRate: bnToString(feeRate),
managerFeeRate: bnToString(managerFeeRate),
};
}

/**
* Single donation using superfluid batch call
* Executes a batch of operations including token approval and calling a function on the pool contract.
Expand Down Expand Up @@ -657,6 +687,7 @@ export class GoodCollectiveSDK {
* @returns {Promise<{protocolFeeBps: number, managerFeeBps: number, managerFeeRecipient: string}>} A promise that resolves to fee information.
*/
async getCollectiveFees(poolAddress: string) {
// Legacy behavior: derive protocol and manager fee bps via factories and pool
try {
// Check if contracts are properly initialized
if (this.factory.address === ethers.constants.AddressZero) {
Expand Down
Loading