From e11bcc72014162354ce6a9a84b04d7441cd93bbb Mon Sep 17 00:00:00 2001 From: sirpy Date: Thu, 12 Feb 2026 19:04:04 +0200 Subject: [PATCH 1/4] add: ui functionality for bulk add + deployed dev env contracts --- .../hooks/managePool/useMemberManagement.ts | 67 +-- .../app/src/pages/ManageCollectivePage.tsx | 52 ++- .../DirectPaymentsFactory.json | 38 +- .../DirectPaymentsFactory_Implementation.json | 104 +++-- .../DirectPaymentsLibrary.json | 26 +- .../development-celo/DirectPaymentsPool.json | 194 ++++---- .../deployments/development-celo/UBIPool.json | 186 ++++---- .../development-celo/UBIPoolFactory.json | 38 +- .../UBIPoolFactory_Implementation.json | 94 ++-- packages/contracts/hardhat.config.ts | 2 +- packages/contracts/releases/deployment.json | 442 ++++++++++++------ 11 files changed, 813 insertions(+), 430 deletions(-) diff --git a/packages/app/src/hooks/managePool/useMemberManagement.ts b/packages/app/src/hooks/managePool/useMemberManagement.ts index 3f39bdc9..4bf4264f 100644 --- a/packages/app/src/hooks/managePool/useMemberManagement.ts +++ b/packages/app/src/hooks/managePool/useMemberManagement.ts @@ -14,8 +14,16 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem const provider = useEthersProvider({ chainId }); const signer = useEthersSigner({ chainId }); + // Memoize SDK instance for efficient reuse + const sdk = useMemo(() => { + const chainIdString = chainId.toString() as `${SupportedNetwork}`; + const network = SupportedNetworkNames[chainId as SupportedNetwork]; + return new GoodCollectiveSDK(chainIdString, provider as any, { network }); + }, [chainId, provider]); + const [memberInput, setMemberInput] = useState(''); const [memberError, setMemberError] = useState(null); + const [memberSuccess, setMemberSuccess] = useState(null); const [isAddingMembers, setIsAddingMembers] = useState(false); const [isRemovingMember, setIsRemovingMember] = useState(false); const [managedMembers, setManagedMembers] = useState([]); @@ -26,7 +34,6 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return; } - // Use the pre-loaded member list from parent component if (initialMembers) { setManagedMembers(initialMembers); setTotalMemberCount(initialMembers.length); @@ -38,7 +45,7 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return Array.from( new Set( memberInput - .split(',') + .split(/[\n,]+/) .map((a) => a.trim()) .filter((a) => a.length > 0) .map((a) => a.toLowerCase()) @@ -46,12 +53,12 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem ); }, [memberInput]); - const validateMemberAddresses = (): string | null => { - if (!parsedMemberAddresses.length) { + const validateAddresses = (addresses: string[]): string | null => { + if (!addresses.length) { return 'Please enter at least one wallet address.'; } - const invalid = parsedMemberAddresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr)); + const invalid = addresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr)); if (invalid) { return `Invalid wallet address: ${invalid}`; } @@ -59,76 +66,76 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem return null; }; - const handleAddMembers = async () => { + const handleAddMembers = async (addressesToAdd: string[]) => { setMemberError(null); - const error = validateMemberAddresses(); + setMemberSuccess(null); + const error = validateAddresses(addressesToAdd); if (error) { setMemberError(error); return; } - if (!signer || !poolAddress || pooltype !== 'UBI' || !provider) { - setMemberError('Member management is currently supported for UBI pools only.'); + if (!signer || !poolAddress || !pooltype || !provider) { + setMemberError('Pool management is not fully initialized.'); + return; + } + + if (pooltype !== 'UBI' && pooltype !== 'DIRECT') { + setMemberError('Member management is currently supported for UBI and Direct Payments pools only.'); return; } try { setIsAddingMembers(true); - const chainIdString = chainId.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chainId as SupportedNetwork]; + const extraData = addressesToAdd.map(() => '0x'); // Empty bytes for extraData - const sdk = new GoodCollectiveSDK(chainIdString, provider, { network }); - - // Use SDK method to add members - for (const addr of parsedMemberAddresses) { - const tx = await sdk.addUBIPoolMember(signer, poolAddress, addr); - await tx.wait(); - } + // Use the SDK's addPoolMembers function for bulk addition + const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData); + await tx.wait(); - // Optimistically bump the total on-chain member count - setTotalMemberCount((prev) => (prev ?? 0) + parsedMemberAddresses.length); + setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length); setManagedMembers((prev) => { const next = new Set(prev.map((a) => a.toLowerCase())); - parsedMemberAddresses.forEach((a) => next.add(a)); + addressesToAdd.forEach((a) => next.add(a)); return Array.from(next); }); setMemberInput(''); + setMemberSuccess(`Successfully added ${addressesToAdd.length} members.`); } catch (e: any) { setMemberError(e?.reason || e?.message || 'Failed to add members.'); + setMemberSuccess(null); } finally { setIsAddingMembers(false); } }; const handleRemoveMember = async (member: string) => { + setMemberError(null); + setMemberSuccess(null); if (!signer || !poolAddress || pooltype !== 'UBI' || !provider) { - setMemberError('Member management is currently supported for UBI pools only.'); + setMemberError('Member removal is currently supported for UBI pools only.'); return; } try { setIsRemovingMember(true); - const chainIdString = chainId.toString() as `${SupportedNetwork}`; - const network = SupportedNetworkNames[chainId as SupportedNetwork]; - - const sdk = new GoodCollectiveSDK(chainIdString, provider, { network }); - - // Use SDK method to remove member + // Use the memoized SDK instance to remove a member const tx = await sdk.removeUBIPoolMember(signer, poolAddress, member); await tx.wait(); - // Optimistically decrease the total on-chain member count setTotalMemberCount((prev) => { if (prev === null) return prev; return prev > 0 ? prev - 1 : 0; }); setManagedMembers((prev) => prev.filter((m) => m.toLowerCase() !== member.toLowerCase())); + setMemberSuccess(`Successfully removed member: ${member}`); } catch (e: any) { setMemberError(e?.reason || e?.message || 'Failed to remove member.'); + setMemberSuccess(null); } finally { setIsRemovingMember(false); } @@ -138,11 +145,13 @@ export const useMemberManagement = ({ poolAddress, pooltype, chainId, initialMem memberInput, setMemberInput, memberError, + memberSuccess, isAddingMembers, isRemovingMember, managedMembers, totalMemberCount, handleAddMembers, handleRemoveMember, + parsedMemberAddresses, }; }; diff --git a/packages/app/src/pages/ManageCollectivePage.tsx b/packages/app/src/pages/ManageCollectivePage.tsx index ebd54a81..66907848 100644 --- a/packages/app/src/pages/ManageCollectivePage.tsx +++ b/packages/app/src/pages/ManageCollectivePage.tsx @@ -1,4 +1,4 @@ -import { HStack, Input, ScrollView, Spinner, Switch, Text, VStack } from 'native-base'; +import { HStack, Input, ScrollView, Spinner, Switch, Text, VStack, TextArea } from 'native-base'; // Add TextArea import { useMemo, useState } from 'react'; import { useParams } from 'react-router-native'; import { useAccount } from 'wagmi'; @@ -417,30 +417,36 @@ const ManageCollectivePage = () => { ) : ( - {/* Add New Member Section */} - + {/* Bulk Add Members Section */} + + Paste a list of wallet addresses, separated by commas or new lines. - New Member Wallet Address - - - - + Wallet Addresses +