Skip to content

updated create-pool and implemented bulk addMember#318

Open
0xcoredev wants to merge 24 commits into
GoodDollar:masterfrom
0xcoredev:create-pool-flow
Open

updated create-pool and implemented bulk addMember#318
0xcoredev wants to merge 24 commits into
GoodDollar:masterfrom
0xcoredev:create-pool-flow

Conversation

@0xcoredev

@0xcoredev 0xcoredev commented Mar 11, 2026

Copy link
Copy Markdown

#317
Title: Create‑pool copy refresh + bulk add members

Summary:

  • Updated create‑pool screens with Figma copy.
  • Added initial members input and bulk add via SDK in create‑pool flow
  • Terms of Use is now clickable

Testing:

  • yarn workspace @gooddollar/goodcollective-contracts compile
  • yarn workspace @gooddollar/goodcollective-sdk build

Summary by Sourcery

Refresh the create-pool flow copy and add support for specifying and bulk-adding initial pool members.

New Features:

  • Allow pool creators to optionally input initial member wallet addresses when configuring a pool.
  • Bulk-add initial pool members to the newly created pool via the GoodCollective SDK.
  • Make the Terms of Use in the welcome screen clickable, including a direct link to the terms URL.

Enhancements:

  • Update welcome and pool type selection copy to match current product messaging and clarify available pool types.
  • Add validation for initial member wallet addresses, including format checks and maximum member count constraints.
  • Adjust pool configuration logic so closed pools are treated as members-only when creating UBI settings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

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 Welcome.tsx, the terms of use are rendered as two separate clickable Text elements with identical onPress handlers; consider consolidating this into a single clearly-labeled touch target to reduce duplication and improve accessibility/UX.
  • The projectId generation in CreatePoolProvider uses replace(' ', '/'), which only replaces the first space; if multiple spaces are expected, consider using a global regex or split/join to normalize the name consistently.
  • In usePoolConfigurationValidation, the comparison members.length > maximumMembers will silently do nothing when maximumMembers is undefined; if this check should only run when a max is explicitly set, consider guarding it with an explicit maximumMembers != null to make the intent clearer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Welcome.tsx`, the terms of use are rendered as two separate clickable `Text` elements with identical `onPress` handlers; consider consolidating this into a single clearly-labeled touch target to reduce duplication and improve accessibility/UX.
- The `projectId` generation in `CreatePoolProvider` uses `replace(' ', '/')`, which only replaces the first space; if multiple spaces are expected, consider using a global regex or `split/join` to normalize the name consistently.
- In `usePoolConfigurationValidation`, the comparison `members.length > maximumMembers` will silently do nothing when `maximumMembers` is undefined; if this check should only run when a max is explicitly set, consider guarding it with an explicit `maximumMembers != null` to make the intent clearer.

## Individual Comments

### Comment 1
<location path="packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx" line_range="105-113" />
<code_context>
       return false;
     }

+    const rawRecipients = form.poolRecipients?.trim() ?? '';
+    const memberAddresses = rawRecipients ? parseMemberAddresses(rawRecipients) : [];
+    if (rawRecipients) {
+      const memberError = validateMemberAddresses(memberAddresses);
+      if (memberError) {
+        console.error(memberError);
+        return false;
+      }
+      if (form.maximumMembers && memberAddresses.length > form.maximumMembers) {
+        console.error(`Members count (${memberAddresses.length}) exceeds maximum (${form.maximumMembers}).`);
+        return false;
</code_context>
<issue_to_address>
**suggestion:** Recipient validation logic is duplicated here and in `usePoolConfigurationValidation`, which risks drift.

Parsing and validating recipient addresses (including the `maximumMembers` length check) now happens both in this context and in `usePoolConfigurationValidation`. This duplication makes it easy for rules (regex, formatting, limits) to diverge. Consider centralizing this in a shared utility—building on `parseMemberAddresses`/`validateMemberAddresses`—and having both the hook and context call the same higher-level validator.

Suggested implementation:

```typescript
    const sdk = new GoodCollectiveSDK(chainIdString, signer.provider as ethers.providers.Provider, { network });

    const validatePoolRecipients = (
      rawRecipients: string | undefined,
      maximumMembers?: number,
    ): {
      isValid: boolean;
      memberAddresses: string[];
      error?: string;
    } => {
      const trimmedRecipients = rawRecipients?.trim() ?? '';

      if (!trimmedRecipients) {
        return { isValid: true, memberAddresses: [] };
      }

      const memberAddresses = parseMemberAddresses(trimmedRecipients);
      const memberError = validateMemberAddresses(memberAddresses);

      if (memberError) {
        return { isValid: false, memberAddresses, error: memberError };
      }

      if (maximumMembers && memberAddresses.length > maximumMembers) {
        return {
          isValid: false,
          memberAddresses,
          error: `Members count (${memberAddresses.length}) exceeds maximum (${maximumMembers}).`,
        };
      }

      return { isValid: true, memberAddresses };
    };

    // Final form validation

```

```typescript
    const recipientsValidation = validatePoolRecipients(form.poolRecipients, form.maximumMembers);

    if (!recipientsValidation.isValid) {
      if (recipientsValidation.error) {
        console.error(recipientsValidation.error);
      }
      return false;
    }

    const { memberAddresses } = recipientsValidation;

```

To fully address the duplication with `usePoolConfigurationValidation`, you should:
1. Move `validatePoolRecipients` into a shared utility module (for example, `packages/app/src/utils/poolRecipients.ts`) so it is not tied to this context.
2. Update this file to import `validatePoolRecipients` from that shared module instead of defining it inline.
3. Update `usePoolConfigurationValidation` to call the shared `validatePoolRecipients` helper (and remove its local recipient parsing/validation logic), ensuring both the hook and this context share the same rules and error messages.
</issue_to_address>

### Comment 2
<location path="packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx" line_range="150-156" />
<code_context>
     // Calculate cycle length based on claim frequency
     const cycleLengthDays = form.claimFrequency && form.claimFrequency <= 7 ? 7 : form.claimFrequency || 1;

+    const onlyMembers = form.joinStatus === 'closed';
     const ubiSettings: UBISettings = {
       claimPeriodDays: ethers.BigNumber.from(form.claimFrequency || 1),
       minActiveUsers: ethers.BigNumber.from(1),
       maxClaimAmount: ethers.utils.parseEther(String(form.claimAmountPerWeek || 0)),
       maxMembers: form.maximumMembers || form.expectedMembers || 100,
-      onlyMembers: form.poolRecipients ? form.canNewMembersJoin ?? false : false,
+      onlyMembers,
       cycleLengthDays: ethers.BigNumber.from(cycleLengthDays),
       claimForEnabled: false,
</code_context>
<issue_to_address>
**question:** Revisiting the `onlyMembers` logic now that it’s based solely on `joinStatus`.

Previously, `onlyMembers` also depended on `form.poolRecipients` being set, but now it’s purely `joinStatus === 'closed'`. With this change, a pool that is `closed` and has no initial members will still have `onlyMembers = true`, which may prevent anyone from ever joining. If that’s not the intended behavior, consider conditioning `onlyMembers` on both `joinStatus` and the presence of initial members, or clearly define and enforce the expected behavior for empty-member pools.
</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 thread packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx Outdated
Comment thread packages/app/src/hooks/useCreatePool/CreatePoolContext.tsx
@0xcoredev

Copy link
Copy Markdown
Author

Hi @L03TJ3 , I just made a video walkthrough showing the functional fixes
Video Demo

@L03TJ3 L03TJ3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Homepage

  1. homepage styles are not aligned with figma (no emphasising with bold text). also, test on mobile screens. the spacing of the words is weird.

  2. the confirm terms of use should be a clickable link, not include the link (figma was just showing where the link should lead to. So: terms of use itself should be clickable, redirecting to > url

  3. the checkbox should be confirmed before able to click on get started

create a pool screen

  1. the link is not clickable

@0xcoredev

Copy link
Copy Markdown
Author

Hi @L03TJ3 ,,,,
I have resolved the homepage and create‑pool feedback, updated the mobile view to match the styles on, made Telegram link clickable, and enforced checkbox gating before Get Started.

@0xcoredev 0xcoredev requested a review from L03TJ3 March 20, 2026 10:27
@L03TJ3

L03TJ3 commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

Hey, sorry that I see this so late. but you produced a demo-video but not actually launched the pool?
how has it been verified that the bulkAdd members worked?
@iYukay

@0xcoredev

0xcoredev commented Mar 27, 2026

Copy link
Copy Markdown
Author

Hi @L03TJ3. I attempted to launch with multiple initial members, but the flow fails at bulk‑add with addMembers is not a function. It looks like the SDK contract instance is using an ABI that doesn’t include addMembers, so I can’t verify. I want to clarify if am supposed to work on the sdk too?

@L03TJ3

L03TJ3 commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Okay. Lets do it like this.
The main bulk of this work has been confirmed to be changed accordingly. (the copy updates)

So we just have to test the bulk-add.

you can pull in the changes of this pull-request (your other assigned item): #315

and include the changes here.
And test both creating a pool with using a list of members + testing the member admin area and bulkAdd members.
this will be the final pull-request and we will close the other. (you will get your reward as has been assigned)
@iYukay

@0xcoredev

Copy link
Copy Markdown
Author

Alright @L03TJ3

Makes sense

@0xcoredev

0xcoredev commented Mar 28, 2026

Copy link
Copy Markdown
Author

Test Summary – PR #318 / #314

I tested both bulk-add flows related to #314:

  • Adding multiple members during pool creation
  • Bulk adding members after pool deployment from the Member Management tab

Important Note on UBI Pools
For UBI pools, added addresses must pass the pool’s uniqueness validator. Random wallet addresses were rejected. So I used eligible addresses for successful tests. Ineligible addresses now show clear error messages in the UI.

Results

  • Bulk add works from the UI after a pool is deployed
  • Multiple addresses can be submitted in one action
  • Address parsing and validation work correctly
  • The UI now gives clearer loading, success, and error feedback
  • Manager action visibility is more stable during refresh
  • Member data refresh behavior is improved after add/remove actions

Demo Video
https://www.loom.com/share/83d52b659d024f638223d219aec1e1ca

@L03TJ3


const eligibleAmount = BigInt(claimAmountStr || '0');

// Check if user has actually claimed by calling hasClaimed(address) on the contract

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why removing comments?

joinStatus?: 'closed' | 'open';
};

export const parseMemberAddresses = (input?: string): string[] => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is a direct replication of the localized logic in useMemberManagement.

Please make it global helpers and apply here and also in useMemberManagement

return Array.from(new Set(normalized));
};

export const validateMemberAddresses = (members: string[]): string | null => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

there is viem.isAddress, no need for localized check

Comment thread packages/app/src/hooks/managePool/useMemberManagement.ts Outdated
@0xcoredev

Copy link
Copy Markdown
Author

Updates made:

  • extracted member address parsing/validation into a shared helper
  • switched address validation to viem.isAddress
  • improved the create flow so initial members are checked earlier and show inline eligibility feedback before launch
  • kept the final validation guard in the create flow as well

One open point on the member list refresh logic:
the data currently passed down to this screen appears to be stewards / stewardCollectives, which reflects activity rather than a true current member list.

Do we already have a dedicated subgraph source for current pool members that you want me to use here instead?

@0xcoredev 0xcoredev requested a review from L03TJ3 April 7, 2026 19:53
@L03TJ3 L03TJ3 requested review from a team and 0xOlivanode and removed request for a team and L03TJ3 April 23, 2026 16:07
@L03TJ3 L03TJ3 moved this to Ready-For-Assignment in GoodBounties Apr 24, 2026
@0xcoredev

Copy link
Copy Markdown
Author

The build errors have been resolved
Please trigger a re-run of the preview workflow at your end to confirm.

Also wanted to flag. my Telegram account has been temporarily frozen so I have no way to reach you there at the moment. GitHub is the best way to get me for now. Apologies for any inconvenience @L03TJ3

@L03TJ3

L03TJ3 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

for security purposes we recently introduced that commits have to be signed.
https://github.com/GoodDollar/.github/blob/master/CONTRIBUTING.md#gpg-signing

for your long list of commits the following command should work after you have setup the gpg signing:
git rebase --exec 'git commit --amend --no-edit -S' -i <your branch>

0xcoredev added 24 commits June 12, 2026 15:58
Address the four blockers raised on PR GoodDollar#318:

1. onlyMembers guard: block deploying a closed pool with zero initial
   members. Validation now errors on the poolRecipients field when
   joinStatus is 'closed' and no addresses are provided, and createPool
   re-checks the same invariant before deploy as defense in depth.
   Without this, a closed pool could deploy with onlyMembers=true and
   no members - permanently inaccessible.

2. Chain-aware uniqueness validator: replace the hardcoded mainnet
   IdentityV2 address (0xC361A6E67822a0EDc17D899227dd9FC50BD62F42) with
   getUniquenessValidatorAddress(chainId), which resolves the address
   from the @gooddollar/goodprotocol deployment manifest by chainId
   (and by REACT_APP_NETWORK on Celo mainnet for staging/dev/QA).
   Returns AddressZero for unknown chains so existing isZeroAddress
   checks treat it as 'no uniqueness check'. Used in both
   PoolConfiguration eligibility checks and CreatePoolContext deploy.

3. Drop fragile status[7] tuple indexing: useMemberManagement now reads
   status.membersCount by name. The typechain UBIPool.status() return
   type already exposes membersCount as a named field, so the
   'as unknown as { membersCount?: BigNumber }' cast and the status[7]
   positional fallback were both redundant and would have broken
   silently if the contract signature changed.

4. Recoverable two-step create flow: when the pool deploys but the
   follow-up addPoolMembers tx fails, throw a typed PoolMembersAddError
   carrying the deployed address. ReviewLaunch detects it and shows a
   dedicated 'POOL CREATED, MEMBERS NOT ADDED' modal with the failure
   reason and a button that navigates to /collective/<address>/manage
   so the user can retry adding members instead of being stranded with
   a deployed but empty pool.
Per PR review: the link previously rendered the full URL
(https://ubi.gd/GoodBuildersTG) as the visible link text. Replace it
with the natural phrase 'reach out here' so the surrounding sentence
reads cleanly and the URL is only present in the href.
Per PR review: membersValidator was still written as a literal
ethers.constants.AddressZero at two call sites (PoolConfiguration
eligibility preview and CreatePoolContext deploy), so it could drift
between the two and could not be configured per-chain without a code
change.

Add getMembersValidatorAddress(chainId) in models/constants.ts and use
it at both call sites. The default return value remains AddressZero
because that is the protocol-correct default - the pool contracts
explicitly skip the validator call when the address is zero (see
UBIPool.sol:286-288 and DirectPaymentsPool.sol:216-218), and there is
no canonical members-validator deployment in either the goodprotocol
or contracts deployment manifests.

The helper supports a per-chain env override (REACT_APP_MEMBERS_VALIDATOR_<chainId>)
with viem-style isAddress validation, so a future deployment can plug
in a custom IMembersValidator without touching the create-pool flow.

This mirrors the chain-aware pattern already used for
getUniquenessValidatorAddress and removes the duplicated literal that
the reviewer flagged.
Was 'development-celo', which made anything reading env.REACT_APP_NETWORK
without an override resolve to the dev GoodDollar deployment. After
making the uniqueness validator chain-aware that meant local builds
without an .env hit the dev Identity contract, where prod-verified
addresses aren't whitelisted. Most other call sites already had a
'|| development-celo' fallback so this only affects the no-env case.
Upstream/master keeps this at development-celo by design - production
deploys override it via env vars at build time. The previous switch to
production-celo here was meant to make the local dev server work without
an .env but diverged from upstream conventions. Going back; for local
production testing set REACT_APP_NETWORK=production-celo in a local .env.
IdentityV2 is a chain-level singleton in practice - all GoodDollar
deployment variants on Celo mainnet (production / staging / development /
pre-production) gate whitelist state off the production registry. The
previous helper re-ordered candidates by REACT_APP_NETWORK, which meant
locally without an .env it would resolve to the dev IdentityV2 (where
prod-verified addresses aren't whitelisted) and silently fail eligibility.

Drop the env re-ordering. Map chainId 42220 -> production-celo and 44787
-> alfajores directly. Same runtime behavior on Celo as the old hardcoded
0xC361... literal, but expressed through the deployment manifest so the
hardcoded-address concern stays addressed.
Reviewer pointed out envs are managed through defaults.ts, not ad-hoc
REACT_APP_MEMBERS_VALIDATOR_<chainId> names per call site.

The MVP doesn't expose a custom members validator anyway - the plan is
to add a form field on the create-pool flow later, falling back to 0x0
when the user leaves it blank. Until then this stays at AddressZero.

Keep the helper as a single source of truth so the eligibility preview
in PoolConfiguration and the deploy-time settings in CreatePoolContext
read from one place instead of duplicating the literal.
Old dev-v1.0.13 endpoint is no longer reachable; the live subgraph is
served at the unpinned 'version/latest' alias (confirmed in PR review).
Without this the homepage stats render as 0 and managePool's member
load silently fails because every query 404s.
The hook was replaying RoleGranted/RoleRevoked logs in the last 9500
blocks (~5h on Celo) to rebuild the member set, so anything older
silently dropped off. Pool data already comes through the subgraph
with the steward list attached, so take that as the steady-state
source and seed managedMembers from it.

Post-tx refresh stays on chain for UBI via sdk.getUBIPoolMembers so
the list updates before the subgraph indexes the events. For
DirectPayments pools we just merge the locally-added addresses and
let the next subgraph poll reconcile - there's no SDK helper for
that pool type.

Also fixes a typo guard that was checking pooltype === 'DIRECT' when
the subgraph emits 'DirectPayments', so the DirectPayments branch
was effectively dead.
Fixed h={120} squashed the textarea on small screens. Switch to minH
so it can expand as the user pastes more addresses, with totalLines
giving native-base a sensible starting size.
isManager is now boolean | undefined where undefined means 'not yet
resolved for the current inputs'. checkingRole is derived from that
plus an in-flight flag, which removes the separate hasResolvedRoleCheck
boolean, the lastResolvedRoleKeyRef, the roleKey join string, and the
preliminary useEffect that only existed to reset the ref.

External return shape stays { isManager: boolean, checkingRole: boolean }
so the callers in ViewCollective and ManageCollectivePage don't change.
The 'don't flash non-manager UI before the on-chain check completes'
behavior the old code was trying to preserve is now expressed by the
checkingRole derivation directly.
When the second tx fails the user has to retype the same addresses on
the manage page, which is painful for any non-trivial member list.
Carry the addresses through PoolMembersAddError and render them in a
scrollable box on the recovery modal with a copy-to-clipboard button.

Clipboard falls back to a hidden textarea + execCommand for older
webviews where navigator.clipboard isn't available.
3a7e01c had the hook read collective.stewardCollectives from the
subgraph, but that entity is only created inside the UBIClaimed and
NFTClaimed handlers (packages/subgraph/src/mappings/ubipool.ts and
mappings/pool.ts). It records claim history, not current membership,
so:
  - members added via addPoolMembers but who haven't claimed yet are
    missing from the list
  - members who were removed but claimed in the past stay in the list

That meant the manage UI would, after a subgraph refresh, drop the
freshly-added members from the create-pool flow.

Switch to sdk.getUBIPoolMembers, which is the helper Lewis pointed
to in the review. It scans MEMBER_ROLE grant/revoke events on the
pool contract and reflects actual membership.

DirectPayments pools have no equivalent SDK helper today; the manage
UI already restricts most editing to UBI, so we keep an empty/local
list there and let the optimistic merge cover add flows.
MembersSectionProps.onValidateRecipients was typed as () => Promise<void>
but the implementation (validateRecipientEligibility) returns
Promise<boolean>. The boolean return value is actually used to gate
submission, so fix the prop type to match.

CreatePoolContext was passing poolSettings.uniquenessValidator and
membersValidator directly to assessPoolMemberEligibility, but
UBIPoolSettings types those fields as PromiseOrValue<string> (SDK
contract type) while the function expects string. Since we set them
from our string-returning helpers (getUniquenessValidatorAddress and
getMembersValidatorAddress), a safe cast is correct here.
@0xcoredev

Copy link
Copy Markdown
Author

Done @L03TJ3 , all my commits are now showing up with the Verified badge. Thank you!!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update: Create a UBI Pool

3 participants