Skip to content

add: ui functionality for bulk add + deployed dev env contracts#315

Open
sirpy wants to merge 2 commits intomasterfrom
feat--add-bulk-members
Open

add: ui functionality for bulk add + deployed dev env contracts#315
sirpy wants to merge 2 commits intomasterfrom
feat--add-bulk-members

Conversation

@sirpy
Copy link
Contributor

@sirpy sirpy commented Feb 12, 2026

Description

#314

Summary by Sourcery

Add bulk member management support in the app and deploy updated development Celo contracts.

New Features:

  • Enable bulk addition of pool members via a textarea input that parses comma- and newline-separated wallet addresses.
  • Allow member management for both UBI and Direct Payments pools via the shared SDK-based handler.

Enhancements:

  • Memoize the GoodCollective SDK instance in member management to reuse it across add/remove operations and improve efficiency.
  • Provide clearer user feedback for member operations with parsed-address counts and success status messages.
  • Simplify core settings hook parameters by removing unused contract configuration input.

Build:

  • Switch the development Celo Hardhat RPC endpoint to the Ankr Celo RPC and update deployment artifacts for Direct Payments and UBI pools on the development-celo network.

Deployment:

  • Refresh releases deployment metadata and development-celo contract deployment JSONs for DirectPayments and UBIPool-related contracts.

@sirpy sirpy linked an issue Feb 12, 2026 that may be closed by this pull request
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In validatePoolSettings, UBIPoolSettings is referenced but never imported/defined and settings.validEvents should likely be directPaymentSettings.validEvents, which will currently cause type or runtime errors.
  • The poolType discriminator in validatePoolSettings uses the string 'DirectPayments', which is inconsistent with the 'DIRECT'/'UBI' values used elsewhere (e.g., in useMemberManagement), so this branch may never execute as intended.
  • In ManageCollectivePage, the TextArea placeholder uses the HTML entity 
 for a newline; in this JSX/NativeBase context you likely want an actual newline ("\n" or {"\n"}) to avoid rendering the literal entity text.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `validatePoolSettings`, `UBIPoolSettings` is referenced but never imported/defined and `settings.validEvents` should likely be `directPaymentSettings.validEvents`, which will currently cause type or runtime errors.
- The `poolType` discriminator in `validatePoolSettings` uses the string `'DirectPayments'`, which is inconsistent with the `'DIRECT'`/`'UBI'` values used elsewhere (e.g., in `useMemberManagement`), so this branch may never execute as intended.
- In `ManageCollectivePage`, the `TextArea` placeholder uses the HTML entity `
` for a newline; in this JSX/NativeBase context you likely want an actual newline (`"\n"` or `{"\n"}`) to avoid rendering the literal entity text.

## Individual Comments

### Comment 1
<location> `packages/app/src/utils/validation.ts:37` </location>
<code_context>
+    if (directPaymentSettings.validEvents.length === 0) {
+      errors.validEvents = 'At least one valid event type is required';
+    }
+    if (directPaymentSettings.rewardPerEvent.length !== settings.validEvents.length) {
+      errors.rewardPerEvent = 'Must specify reward amount for each event type';
+    }
</code_context>

<issue_to_address>
**issue (bug_risk):** The reference to `settings.validEvents` is likely a bug and should use `directPaymentSettings.validEvents` instead.

In `validatePoolSettings`, `settings` is not in scope, so this line will fail at type-check or runtime. This should compare `rewardPerEvent.length` to `directPaymentSettings.validEvents.length` instead.
</issue_to_address>

### Comment 2
<location> `packages/app/src/pages/ManageCollectivePage.tsx:426` </location>
<code_context>
-                  </HStack>
+                  <Text fontWeight="600">Wallet Addresses</Text>
+                  <TextArea
+                    placeholder="0xabc...123, 0xdef...456&#10;0xghi...789"
+                    value={memberManagement.memberInput}
+                    onChangeText={memberManagement.setMemberInput}
</code_context>

<issue_to_address>
**issue (bug_risk):** The use of `&#10;` in the placeholder likely won’t render as a newline in React Native and may show the raw entity.

Because React Native doesn’t parse HTML entities in JSX strings, this will likely render the literal `&#10;` instead of a newline. To get a line break in the placeholder, use an actual newline in a template string or `{"\n"}` between the examples (e.g., `"0xabc...123, 0xdef...456\n0xghi...789"`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

</HStack>
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
placeholder="0xabc...123, 0xdef...456&#10;0xghi...789"
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): The use of &#10; in the placeholder likely won’t render as a newline in React Native and may show the raw entity.

Because React Native doesn’t parse HTML entities in JSX strings, this will likely render the literal &#10; instead of a newline. To get a line break in the placeholder, use an actual newline in a template string or {"\n"} between the examples (e.g., "0xabc...123, 0xdef...456\n0xghi...789").

@L03TJ3
Copy link
Collaborator

L03TJ3 commented Feb 26, 2026

@edehvictor Hey what is the status of this?

@edehvictor
Copy link

edehvictor commented Feb 26, 2026

Gm @L03TJ3, I've fixed it.

@L03TJ3
Copy link
Collaborator

L03TJ3 commented Feb 27, 2026

@edehvictor did you test the PR? any comments or confirmation about the flow working?
The request was: You can assigned to fix, review and test the PR for a common tier.

its a code-review + qa type request.

@edehvictor edehvictor marked this pull request as ready for review March 1, 2026 22:22
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The memoized GoodCollectiveSDK is created even when provider or chainId might be undefined/invalid; consider guarding the useMemo (or returning null and checking before use) so that SDK construction doesn’t rely on as any and fail at runtime during intermediate loading states.
  • memberSuccess messages are only cleared on add/remove actions and not when the input changes, which can leave stale success messages visible while the user is preparing a new operation; consider resetting memberSuccess when memberInput or parsedMemberAddresses changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The memoized `GoodCollectiveSDK` is created even when `provider` or `chainId` might be undefined/invalid; consider guarding the `useMemo` (or returning `null` and checking before use) so that SDK construction doesn’t rely on `as any` and fail at runtime during intermediate loading states.
- `memberSuccess` messages are only cleared on add/remove actions and not when the input changes, which can leave stale success messages visible while the user is preparing a new operation; consider resetting `memberSuccess` when `memberInput` or `parsedMemberAddresses` changes.

## Individual Comments

### Comment 1
<location path="packages/app/src/hooks/managePool/useMemberManagement.ts" line_range="91-94" />
<code_context>

-      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();

</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider filtering out already-managed members before calling addPoolMembers.

Currently all `addressesToAdd` are sent to `sdk.addPoolMembers`, and only after success are they merged into `managedMembers`. If the contract rejects duplicates or treats them as no-ops, this could cause a revert or unnecessary gas use. Consider pre-filtering `addressesToAdd` against `managedMembers` (using the same lowercasing) so only truly new addresses are passed to the contract.
</issue_to_address>

### Comment 2
<location path="packages/app/src/pages/ManageCollectivePage.tsx" line_range="424-434" />
<code_context>
-                    />
-                  </HStack>
+                  <Text fontWeight="600">Wallet Addresses</Text>
+                  <TextArea
+                    autoCompleteType={''}
+                    placeholder="0xabc...123, 0xdef...456&#10;0xghi...789"
+                    value={memberManagement.memberInput}
+                    onChangeText={memberManagement.setMemberInput}
+                    autoCapitalize="none"
+                    borderRadius={8}
+                    h={120} // Set a fixed height for the textarea
+                  />
+                  <Text fontSize="xs" color="gray.500">
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid using an empty string for autoCompleteType and adjust multiline placeholder formatting.

1) Instead of `autoCompleteType={''}`, omit the prop when you don’t want autocomplete; an empty string can conflict with the prop’s union type and cause platform warnings.
2) `&#10;` in the placeholder will render as literal text in React/React Native. If you want a visible line break, use a template literal or `{'
'}` in the string instead.

```suggestion
                  <Text fontWeight="600">Wallet Addresses</Text>
                  <TextArea
                    placeholder={`0xabc...123, 0xdef...456
0xghi...789`}
                    value={memberManagement.memberInput}
                    onChangeText={memberManagement.setMemberInput}
                    autoCapitalize="none"
                    borderRadius={8}
                    h={120} // Set a fixed height for the textarea
                  />
                  <Text fontSize="xs" color="gray.500">
```
</issue_to_address>

### Comment 3
<location path="packages/app/src/hooks/managePool/useMemberManagement.ts" line_range="62" />
<code_context>
     return null;
   };

-  const handleAddMembers = async () => {
+  const handleAddMembers = async (addressesToAdd: string[]) => {
     setMemberError(null);
</code_context>
<issue_to_address>
**issue (complexity):** Consider keeping parsing and validation internal to the hook, centralizing status and SDK readiness handling to shrink the hook’s API and simplify its control flow.

You can reduce the hook’s surface area and control flow without changing behavior by tightening the boundary around parsing/validation and simplifying status + SDK handling.

### 1. Keep parsing/validation internal to the hook

`handleAddMembers` is always used with `parsedMemberAddresses`. You don’t gain real flexibility from the `addressesToAdd` argument + `validateAddresses(addresses: string[])`, but you do couple the caller to internal parsing.

You can:

- Make `validateAddresses` parameterless and work on `parsedMemberAddresses`.
- Make `handleAddMembers` parameterless and call `validateAddresses` + use `parsedMemberAddresses` internally.
- Optionally keep `parsedMemberAddresses` in the return only for display.

```ts
const validateAddresses = (): string | null => {
  if (!parsedMemberAddresses.length) {
    return 'Please enter at least one wallet address.';
  }
  const invalid = parsedMemberAddresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr));
  if (invalid) {
    return `Invalid wallet address: ${invalid}`;
  }
  return null;
};

const handleAddMembers = async () => {
  clearStatus();
  const error = validateAddresses();
  if (error) {
    setMemberError(error);
    return;
  }

  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;
  }

  const addressesToAdd = parsedMemberAddresses;

  try {
    setIsAddingMembers(true);
    const extraData = addressesToAdd.map(() => '0x');
    const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData);
    await tx.wait();

    setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length);
    setManagedMembers((prev) => {
      const next = new Set(prev.map((a) => a.toLowerCase()));
      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);
  }
};
```

And in the return value, if you don’t actually need to pass addresses into `handleAddMembers` from outside, you can avoid exporting `parsedMemberAddresses` or keep it clearly read-only:

```ts
return {
  memberInput,
  setMemberInput,
  memberError,
  memberSuccess,
  isAddingMembers,
  isRemovingMember,
  managedMembers,
  totalMemberCount,
  handleAddMembers,      // no args
  handleRemoveMember,
  parsedMemberAddresses, // optional, for display only
};
```

### 2. Factor out status clearing

You clear `memberError` and `memberSuccess` in multiple places. A tiny helper removes that noise and makes the intent explicit:

```ts
const clearStatus = () => {
  setMemberError(null);
  setMemberSuccess(null);
};

const handleAddMembers = async () => {
  clearStatus();
  // ...
};

const handleRemoveMember = async (member: string) => {
  clearStatus();
  // ...
};
```

### 3. Optional: centralize SDK readiness check

Right now you have:

- `sdk` memoized on `chainId` and `provider`
- Repeated guards on `!signer || !poolAddress || !pooltype || !provider`

You can keep memoization but centralize the readiness check to avoid duplicating the “not initialized” logic:

```ts
const getSdkOrFail = () => {
  if (!signer || !poolAddress || !pooltype || !provider) {
    throw new Error('Pool management is not fully initialized.');
  }
  return sdk;
};

const handleAddMembers = async () => {
  clearStatus();
  const error = validateAddresses();
  if (error) {
    setMemberError(error);
    return;
  }

  if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
    setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
    return;
  }

  try {
    setIsAddingMembers(true);
    const sdkInstance = getSdkOrFail();
    // use sdkInstance here
  } catch (e: any) {
    if (e.message === 'Pool management is not fully initialized.') {
      setMemberError(e.message);
      return;
    }
    // existing error handling
  } finally {
    setIsAddingMembers(false);
  }
};
```

This keeps the new functionality (bulk add, success messages, DIRECT pools, memoized SDK) intact while reducing the hook’s API surface and internal branching.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +91 to +94
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);
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Consider filtering out already-managed members before calling addPoolMembers.

Currently all addressesToAdd are sent to sdk.addPoolMembers, and only after success are they merged into managedMembers. If the contract rejects duplicates or treats them as no-ops, this could cause a revert or unnecessary gas use. Consider pre-filtering addressesToAdd against managedMembers (using the same lowercasing) so only truly new addresses are passed to the contract.

Comment on lines +424 to +434
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
autoCompleteType={''}
placeholder="0xabc...123, 0xdef...456&#10;0xghi...789"
value={memberManagement.memberInput}
onChangeText={memberManagement.setMemberInput}
autoCapitalize="none"
borderRadius={8}
h={120} // Set a fixed height for the textarea
/>
<Text fontSize="xs" color="gray.500">
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid using an empty string for autoCompleteType and adjust multiline placeholder formatting.

  1. Instead of autoCompleteType={''}, omit the prop when you don’t want autocomplete; an empty string can conflict with the prop’s union type and cause platform warnings.
  2. &#10; in the placeholder will render as literal text in React/React Native. If you want a visible line break, use a template literal or {' '} in the string instead.
Suggested change
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
autoCompleteType={''}
placeholder="0xabc...123, 0xdef...456&#10;0xghi...789"
value={memberManagement.memberInput}
onChangeText={memberManagement.setMemberInput}
autoCapitalize="none"
borderRadius={8}
h={120} // Set a fixed height for the textarea
/>
<Text fontSize="xs" color="gray.500">
<Text fontWeight="600">Wallet Addresses</Text>
<TextArea
placeholder={`0xabc...123, 0xdef...456
0xghi...789`}
value={memberManagement.memberInput}
onChangeText={memberManagement.setMemberInput}
autoCapitalize="none"
borderRadius={8}
h={120} // Set a fixed height for the textarea
/>
<Text fontSize="xs" color="gray.500">

return null;
};

const handleAddMembers = async () => {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider keeping parsing and validation internal to the hook, centralizing status and SDK readiness handling to shrink the hook’s API and simplify its control flow.

You can reduce the hook’s surface area and control flow without changing behavior by tightening the boundary around parsing/validation and simplifying status + SDK handling.

1. Keep parsing/validation internal to the hook

handleAddMembers is always used with parsedMemberAddresses. You don’t gain real flexibility from the addressesToAdd argument + validateAddresses(addresses: string[]), but you do couple the caller to internal parsing.

You can:

  • Make validateAddresses parameterless and work on parsedMemberAddresses.
  • Make handleAddMembers parameterless and call validateAddresses + use parsedMemberAddresses internally.
  • Optionally keep parsedMemberAddresses in the return only for display.
const validateAddresses = (): string | null => {
  if (!parsedMemberAddresses.length) {
    return 'Please enter at least one wallet address.';
  }
  const invalid = parsedMemberAddresses.find((addr) => !/^0x[a-fA-F0-9]{40}$/.test(addr));
  if (invalid) {
    return `Invalid wallet address: ${invalid}`;
  }
  return null;
};

const handleAddMembers = async () => {
  clearStatus();
  const error = validateAddresses();
  if (error) {
    setMemberError(error);
    return;
  }

  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;
  }

  const addressesToAdd = parsedMemberAddresses;

  try {
    setIsAddingMembers(true);
    const extraData = addressesToAdd.map(() => '0x');
    const tx = await sdk.addPoolMembers(signer, poolAddress, addressesToAdd, extraData);
    await tx.wait();

    setTotalMemberCount((prev) => (prev ?? 0) + addressesToAdd.length);
    setManagedMembers((prev) => {
      const next = new Set(prev.map((a) => a.toLowerCase()));
      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);
  }
};

And in the return value, if you don’t actually need to pass addresses into handleAddMembers from outside, you can avoid exporting parsedMemberAddresses or keep it clearly read-only:

return {
  memberInput,
  setMemberInput,
  memberError,
  memberSuccess,
  isAddingMembers,
  isRemovingMember,
  managedMembers,
  totalMemberCount,
  handleAddMembers,      // no args
  handleRemoveMember,
  parsedMemberAddresses, // optional, for display only
};

2. Factor out status clearing

You clear memberError and memberSuccess in multiple places. A tiny helper removes that noise and makes the intent explicit:

const clearStatus = () => {
  setMemberError(null);
  setMemberSuccess(null);
};

const handleAddMembers = async () => {
  clearStatus();
  // ...
};

const handleRemoveMember = async (member: string) => {
  clearStatus();
  // ...
};

3. Optional: centralize SDK readiness check

Right now you have:

  • sdk memoized on chainId and provider
  • Repeated guards on !signer || !poolAddress || !pooltype || !provider

You can keep memoization but centralize the readiness check to avoid duplicating the “not initialized” logic:

const getSdkOrFail = () => {
  if (!signer || !poolAddress || !pooltype || !provider) {
    throw new Error('Pool management is not fully initialized.');
  }
  return sdk;
};

const handleAddMembers = async () => {
  clearStatus();
  const error = validateAddresses();
  if (error) {
    setMemberError(error);
    return;
  }

  if (pooltype !== 'UBI' && pooltype !== 'DIRECT') {
    setMemberError('Member management is currently supported for UBI and Direct Payments pools only.');
    return;
  }

  try {
    setIsAddingMembers(true);
    const sdkInstance = getSdkOrFail();
    // use sdkInstance here
  } catch (e: any) {
    if (e.message === 'Pool management is not fully initialized.') {
      setMemberError(e.message);
      return;
    }
    // existing error handling
  } finally {
    setIsAddingMembers(false);
  }
};

This keeps the new functionality (bulk add, success messages, DIRECT pools, memoized SDK) intact while reducing the hook’s API surface and internal branching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GoodBounties - Common Standard difficulty

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Bulk Add Members to Pools in UI

3 participants