Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
105 changes: 105 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# GoodCollective Monorepo – AI Agent Reference

## Monorepo Layout

| Path | Description |
| ------------------- | -------------------------------------------------------------- |
| packages/contracts/ | Hardhat Solidity contracts: pool factories, pools, ProvableNFT |
| packages/sdk-js/ | TypeScript SDK: wraps contracts, loads from deployment.json |
| packages/app/ | React Native Web app using NativeBase, G$ support |
| packages/subgraph/ | TheGraph schema and mappings: pools, claims |
| packages/services/ | Docker setup for devnet/Subgraph infra |
| releases/ | deployment.json: contract addresses by network |
| scripts/ | Utility scripts (migrations, transforms) |
| test/ | Cross-package integration tests |

## Smart Contracts - Essential Concepts

- **DirectPaymentsFactory & DirectPaymentsPool**

- Factory deploys pools; links each to a ProvableNFT.
- Uses a UUPS proxy. Updates via `updateImpl`.
- Holds/verifies pool metadata (IPFS).
- Minting NFT triggers reward distribution (optional on mint).

- **UBIPool & UBIPoolFactory**

- Creates and manages UBI pools by project ID.
- Uses UpgradeableBeacon (`updateImpl` affects all).
- Tracks which pool controls a project.
- Small fee (bps) sent to feeRecipient.

- **ProvableNFT**

- ERC-721 (UUPS) for proof-of-action NFTs (linked per pool).
- `mintNFT` may also trigger reward claim.

- **GoodDollar Integration**
- **G$ Token:** All rewards in GoodDollar (ISuperToken).
- **feeRecipient:** Defaults to GoodDollar UBIScheme if available.

## SDK Usage: Always Prefer these Methods!

**Do NOT hardcode contract addresses.** Use the SDK, which loads addresses and ABIs from deployment.json by chainId.

### Key SDK Methods

- `sdk.createPool(...)` – Deploy DirectPaymentsPool via factory.
- `sdk.mintNft(...)` – Mint/generate proof-of-action NFT (optionally claim reward).
- `sdk.supportFlow(...)` – Start Superfluid G$ stream to a pool.
- `sdk.supportSingleTransferAndCall(...)` – One-time donation via ERC677.
- `sdk.supportSingleBatch(...)` – Batch donation via Superfluid.

**Best Practice:**
Let the SDK resolve all addresses. Always load addresses/ABIs by network via the SDK.

## App UI Coding Guide

**Always prefer NativeBase for UI in `packages/app`!**

- Use `<Box>`, `<Text>`, `<HStack>`, etc.
- Style using NativeBase props (e.g. `p={4}`, `bg="goodPurple.400"`).
- Use theme variables (see `app/src/theme`).
- Avoid `StyleSheet.create` for new components; use prop-based styling.
- If necessary, compute style objects _and spread into NativeBase components_.

**Example:**

```tsx
<Box p={4} bg={value === option ? 'goodPurple.400' : 'white'} alignItems="center">
<Text fontSize="lg" color="goodPurple.700">
...
</Text>
</Box>
```

- Responsive props (`w={8} h={8}` etc.) preferred over fixed styles.

## Testing & Workflow

1. **Deploy Local Contracts:**
`yarn test:setup` (sets up contracts, generates latest deployment.json)

2. **Run Tests:**
`yarn test` (executes contract & SDK tests; all must pass)

3. **Lint & Format:**
Follow Prettier/ESLint (2-space, single quotes, <=120 chars).
Make sure typescript compiles by running `tsc` in the affected packages or apps.

4. **Build related packages/apps**
Run build script for the affected package or app.

5. **Pull Requests:**
- Clear description of changes.
- All tests AND lint must pass.

## Agent Coding Principles

- Use SDK for all contract interactions.
- Never hardcode contract addresses; always resolve via deployment.json.
- Style React components with NativeBase/theming, not CSS or StyleSheet for new code.
- Follow existing conventions for file locations, imports, and naming.
- Ensure all relevant tests pass after changes.

---
17 changes: 8 additions & 9 deletions packages/app/src/components/ActivityLog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Image, Text, View, StyleSheet } from 'react-native';
import { Image, Text, View } from 'native-base';
import { InterRegular, InterSemiBold } from '../utils/webFonts';
import { Colors } from '../utils/colors';

const ReceiveIconUri = `data:image/svg+xml;utf8,<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect width="32" height="32" rx="16" fill="#95EED8"/> <path d="M19.048 14.6186C18.7453 14.3159 18.2546 14.3159 17.952 14.6186L16.775 15.7956V9.33325C16.775 8.90523 16.428 8.55825 16 8.55825C15.572 8.55825 15.225 8.90523 15.225 9.33325V15.7956L14.048 14.6186C13.7453 14.3159 13.2546 14.3159 12.952 14.6186C12.6493 14.9212 12.6493 15.4119 12.952 15.7146L15.452 18.2146C15.7546 18.5172 16.2453 18.5172 16.548 18.2146L19.048 15.7146C19.3507 15.4119 19.3507 14.9212 19.048 14.6186ZM23.4417 15.9999C23.4417 15.5719 23.0947 15.2249 22.6667 15.2249C22.2386 15.2249 21.8917 15.5719 21.8917 15.9999C21.8917 19.2538 19.2539 21.8916 16 21.8916C12.7461 21.8916 10.1083 19.2538 10.1083 15.9999C10.1083 15.5719 9.76135 15.2249 9.33333 15.2249C8.90531 15.2249 8.55833 15.5719 8.55833 15.9999C8.55833 20.1098 11.8901 23.4416 16 23.4416C20.1099 23.4416 23.4417 20.1098 23.4417 15.9999Z" fill="#27564B" stroke="#5BBAA3" stroke-width="0.3"/> </svg> `;

Expand Down Expand Up @@ -28,10 +27,10 @@ function ActivityLog({}: ActivityLogProps) {
);
}

const styles = StyleSheet.create({
const styles = {
container: {
width: '100%',
backgroundColor: Colors.white,
backgroundColor: 'white',
paddingLeft: 16,
paddingRight: 16,
},
Expand All @@ -40,10 +39,10 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
gap: 8,
backgroundColor: Colors.white,
backgroundColor: 'white',
},
bar: {
backgroundColor: Colors.green[100],
backgroundColor: 'goodGreen.200',
width: 6,
height: 40,
alignSelf: 'flex-start',
Expand All @@ -53,14 +52,14 @@ const styles = StyleSheet.create({
height: 32,
},
name: {
color: Colors.black,
color: 'black',
fontSize: 16,
lineHeight: 24,
...InterSemiBold,
width: '100%',
},
date: {
color: Colors.gray[100],
color: 'goodGrey.400',
fontSize: 14,
lineHeight: 21,
textAlign: 'left',
Expand All @@ -74,5 +73,5 @@ const styles = StyleSheet.create({
},
logProfileDetails: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
logDate: { alignItems: 'flex-end' },
});
} as const;
export default ActivityLog;
29 changes: 13 additions & 16 deletions packages/app/src/components/BannerPool.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Image, Linking, StyleSheet, TouchableOpacity } from 'react-native';
import { View } from 'native-base';
import { Colors } from '../utils/colors';
import { Linking } from 'react-native';
import { Image, Pressable, View } from 'native-base';
import { DirectPayments, Segmented, Community } from '../assets/poolTypes';

const getPoolType = (pool: string) => {
Expand All @@ -26,30 +25,28 @@ function BannerPool({
poolType: string;
}) {
const poolTypes = getPoolType(poolType);
const imgStyles = !homePage ? (isDesktopView ? styles.imageDesktop : styles.image) : styles.sectionImage;
return (
<View style={styles.imageContainer}>
<Image
source={headerImg}
style={!homePage ? (isDesktopView ? styles.imageDesktop : styles.image) : styles.sectionImage}
/>
<TouchableOpacity
<View {...styles.imageContainer}>
<Image source={headerImg} alt="header" {...imgStyles} />
<Pressable
onPress={(e) => {
e.stopPropagation();
Linking.openURL('https://www.gooddollar.org/goodcollective-how-it-work');
}}>
<View style={styles.poolTypeContainer}>
<Image source={poolTypes} style={styles.poolTypeImg} />
<View {...styles.poolTypeContainer}>
<Image source={poolTypes} alt="pool" {...styles.poolTypeImg} />
</View>
</TouchableOpacity>
</Pressable>
</View>
);
}

const styles = StyleSheet.create({
const styles = {
sectionImage: {
resizeMode: 'cover',
height: 192,
backgroundColor: Colors.white,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
Expand Down Expand Up @@ -80,13 +77,13 @@ const styles = StyleSheet.create({
right: 12,
padding: 5,
cursor: 'pointer',
backgroundColor: '#ffffff',
backgroundColor: 'white',
borderRadius: 8,
},
poolTypeImg: {
width: '100%',
height: '100%',
},
});
} as const;

export default BannerPool;
33 changes: 16 additions & 17 deletions packages/app/src/components/CollectiveHomeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Pressable, Text, View } from 'native-base';

import { InterSemiBold, InterSmall } from '../utils/webFonts';
import useCrossNavigate from '../routes/useCrossNavigate';
import { Colors } from '../utils/colors';
import { Ocean } from '../assets';
import { useScreenSize } from '../theme/hooks';
import BannerPool from './BannerPool';
Expand All @@ -21,29 +20,29 @@ function CollectiveHomeCard({ name, description, headerImage, route, poolType }:

const headerImg = headerImage ? { uri: headerImage } : Ocean;

const containerStyle = {
...styles.cardContainer,
...styles.elevation,
...(isDesktopView ? styles.cardContainerDesktop : {}),
...(isTabletView ? { marginBottom: 20 } : {}),
};

return (
<TouchableOpacity
style={[
styles.cardContainer,
styles.elevation,
isDesktopView ? styles.cardContainerDesktop : {},
isTabletView ? { marginBottom: 20 } : {},
]}
onPress={() => navigate(`/collective/${route}`)}>
<Pressable {...containerStyle} onPress={() => navigate(`/collective/${route}`)}>
<BannerPool isDesktopView={isDesktopView} poolType={poolType} headerImg={headerImg} homePage={true} />
<View style={styles.cardDescriptionContainer}>
<Text style={styles.cardTitle}>{name}</Text>
<Text style={styles.cardDescription}>{description}</Text>
</View>
</TouchableOpacity>
</Pressable>
);
}

const styles = StyleSheet.create({
const styles = {
cardContainer: {
width: '100%',
minHeight: 330,
backgroundColor: Colors.white,
backgroundColor: 'white',
paddingTop: 0,
borderRadius: 20,
marginBottom: 20,
Expand All @@ -65,7 +64,7 @@ const styles = StyleSheet.create({
marginTop: 8,
fontSize: 16,
fontWeight: '400',
color: Colors.gray[100],
color: 'goodGrey.400',
...InterSmall,
lineHeight: 24,
maxHeight: 'auto',
Expand All @@ -74,7 +73,7 @@ const styles = StyleSheet.create({
sectionImage: {
resizeMode: 'cover',
height: 192,
backgroundColor: Colors.white,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
Expand All @@ -84,7 +83,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 12,
},
elevation: {
shadowColor: Colors.black,
shadowColor: 'black',
shadowOffset: {
width: 0,
height: 12,
Expand All @@ -93,6 +92,6 @@ const styles = StyleSheet.create({
shadowRadius: 15,
elevation: 24,
},
});
} as const;

export default CollectiveHomeCard;
9 changes: 5 additions & 4 deletions packages/app/src/components/DonorsList/DonorsList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Platform, StyleSheet, View } from 'react-native';
import { Platform } from 'react-native';
import { View } from 'native-base';
import { DonorCollective } from '../../models/models';
import { DonorsListItem } from './DonorsListItem';
import { useMemo } from 'react';
Expand All @@ -22,15 +23,15 @@ function DonorsList({ donors, listStyle }: DonorsListProps) {
const userFullNames = useFetchFullNames(userAddresses);

return (
<View style={[styles.list, { ...(listStyle ?? {}) }]}>
<View {...styles.list} {...(listStyle ?? {})}>
{sortedDonors.map((donor, index) => (
<DonorsListItem key={donor.donor} donor={donor} rank={index + 1} userFullName={userFullNames[donor.donor]} />
))}
</View>
);
}

const styles = StyleSheet.create({
const styles = {
donorsListContainer: { width: '100%' },
list: {
width: '100%',
Expand All @@ -41,6 +42,6 @@ const styles = StyleSheet.create({
default: 'auto',
}),
},
});
} as const;

export default DonorsList;
Loading
Loading