Skip to content

Commit 0fa143f

Browse files
authored
Feat/adds optional constant funding per grant in retrofunding (#56)
* fix: toaster errors * feat: adds constant amount for each grant on retrofunding distribute component * chore: added a new story to simulate GG23 Distributions * added changeset
1 parent cc67862 commit 0fa143f

File tree

8 files changed

+125
-18
lines changed

8 files changed

+125
-18
lines changed

.changeset/soft-carpets-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gitcoin/ui": patch
3+
---
4+
5+
Adds optional constant funding per grantee in retrofunding Distribute component

packages/ui/src/components/Toaster/Toaster.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const Toaster = () => {
5757
.otherwise(() => <Icon type={IconType.SOLID_X} className="size-5 rounded-full" />);
5858
return (
5959
<Toast
60+
key={toast.id}
6061
toast={{
6162
...toast,
6263
icon: ToastIcon ?? IconType.SOLID_X,

packages/ui/src/features/retrofunding/components/Distribute/Distribute.stories.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { action } from "@storybook/addon-actions";
22
import { StoryObj, Meta } from "@storybook/react";
3-
import { parseUnits } from "viem";
3+
import { formatUnits, parseUnits } from "viem";
44

55
import { PoolStatus } from "@/types";
66
import { ApplicationPayout, PoolConfig } from "@/types/distribute";
@@ -19,11 +19,12 @@ const TOKEN_DECIMALS = 18;
1919

2020
const POOL_CONFIG: PoolConfig = {
2121
tokenTicker: "ETH",
22-
amountOfTokensInPool: parseUnits("10", TOKEN_DECIMALS).toString(),
22+
amountOfTokensInPool: parseUnits("100", TOKEN_DECIMALS).toString(),
2323
amountOfTokensToDistribute: 100,
2424
tokenDecimals: TOKEN_DECIMALS,
2525
poolStatus: PoolStatus.FundingPending,
2626
chainId: 11155111,
27+
constantAmountPerGrant: 0,
2728
};
2829

2930
const MOCK_APPLICATIONS: ApplicationPayout[] = [
@@ -32,28 +33,28 @@ const MOCK_APPLICATIONS: ApplicationPayout[] = [
3233
title: "Project Alpha",
3334
imageUrl: "https://picsum.photos/100",
3435
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
35-
payoutPercentage: 23,
36+
payoutPercentage: 10.4,
3637
},
3738
{
3839
id: "2",
3940
title: "Long Project Title That Might Need Truncation",
4041
imageUrl: "https://picsum.photos/101",
4142
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
42-
payoutPercentage: 23,
43+
payoutPercentage: 14.6,
4344
},
4445
{
4546
id: "3",
4647
title: "Project Beta",
4748
imageUrl: "https://picsum.photos/102",
4849
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
49-
payoutPercentage: 23,
50+
payoutPercentage: 23.7,
5051
},
5152
{
5253
id: "4",
5354
title: "Project Gamma",
5455
imageUrl: "https://picsum.photos/103",
5556
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
56-
payoutPercentage: 23,
57+
payoutPercentage: 22.3,
5758
},
5859
{
5960
id: "5",
@@ -77,14 +78,14 @@ const MOCK_APPLICATIONS: ApplicationPayout[] = [
7778
title: "Project Zeta",
7879
imageUrl: "https://picsum.photos/106",
7980
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
80-
payoutPercentage: 2,
81+
payoutPercentage: 13.4,
8182
},
8283
{
8384
id: "8",
8485
title: "Project Epsilon",
8586
imageUrl: "https://picsum.photos/107",
8687
payoutAddress: "0x4614291bb169905074Da4aFaA39784D175162f79",
87-
payoutPercentage: 2,
88+
payoutPercentage: 11.6,
8889
},
8990
];
9091

@@ -93,7 +94,17 @@ const args = {
9394
poolConfig: { ...POOL_CONFIG, amountOfTokensInPool: parseUnits("96", TOKEN_DECIMALS).toString() },
9495
canResetEdit: true,
9596
onFundRound: async (values: any) => onFundRound(values),
96-
onDistribute: async (values: any) => onDistribute(values),
97+
onDistribute: async (values: any) => {
98+
const finalAmount = values.reduce((acc: any, curr: any) => {
99+
acc += curr.amount;
100+
return acc;
101+
}, 0n);
102+
onDistribute(
103+
"Amount to distribute: ",
104+
formatUnits(finalAmount, POOL_CONFIG.tokenDecimals),
105+
values,
106+
);
107+
},
97108
onEditPayouts: async (values: any) => onEditPayouts(values),
98109
onResetEdit: async () => onResetEdit(),
99110
className: "w-full",
@@ -153,6 +164,23 @@ export const NoFinalizedProjects: StoryObj<typeof Distribute> = {
153164
},
154165
};
155166

167+
export const NoFinalizedProjectsWithConstantAmountPerGrant: StoryObj<typeof Distribute> = {
168+
args: {
169+
...args,
170+
applications: MOCK_APPLICATIONS.map((application) => ({
171+
...application,
172+
payoutTransactionHash: undefined,
173+
})),
174+
poolConfig: {
175+
...POOL_CONFIG,
176+
constantAmountPerGrant: 10000,
177+
amountOfTokensInPool: parseUnits("300000", POOL_CONFIG.tokenDecimals).toString(),
178+
amountOfTokensToDistribute: 300000,
179+
tokenTicker: "USDC",
180+
},
181+
},
182+
};
183+
156184
export const NotFundingPhase: StoryObj<typeof Distribute> = {
157185
args: {
158186
...args,

packages/ui/src/features/retrofunding/components/Distribute/Distribute.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ApplicationPayout, PoolConfig } from "@/types/distribute";
77
import { FundRoundSection, ActionButtons } from "./components";
88
import { DistributeTabs } from "./components/DistributeTabs";
99
import { useRound } from "./hooks/useRound";
10-
import { formatAmountFromPercentage } from "./utils";
10+
import { formatAmountFromPercentageWithConstant, getAvailableTokensToDistribute } from "./utils";
1111

1212
interface DistributeProps {
1313
applications: ApplicationPayout[];
@@ -42,6 +42,11 @@ export const Distribute = ({
4242
[applications],
4343
);
4444

45+
const availableTokensToDistribute = getAvailableTokensToDistribute(
46+
applications.length,
47+
poolConfig,
48+
);
49+
4550
const canEdit = finalizedApplications.length === 0 && pendingApplications.length > 0;
4651
const hasPendingApplications = pendingApplications.length > 0;
4752
const distributionCompleted =
@@ -53,10 +58,11 @@ export const Distribute = ({
5358
.filter((p) => selectedApplications.includes(p.id))
5459
.map((p) => ({
5560
applicationId: p.id,
56-
amount: formatAmountFromPercentage(
57-
poolConfig.amountOfTokensToDistribute,
61+
amount: formatAmountFromPercentageWithConstant(
62+
availableTokensToDistribute,
5863
p.payoutPercentage,
5964
poolConfig.tokenDecimals,
65+
poolConfig.constantAmountPerGrant,
6066
),
6167
})),
6268
);
@@ -65,10 +71,11 @@ export const Distribute = ({
6571
const totalPaid = finalizedApplications.reduce(
6672
(acc, curr) =>
6773
acc +
68-
formatAmountFromPercentage(
69-
poolConfig.amountOfTokensToDistribute,
74+
formatAmountFromPercentageWithConstant(
75+
availableTokensToDistribute,
7076
curr.payoutPercentage,
7177
poolConfig.tokenDecimals,
78+
poolConfig.constantAmountPerGrant,
7279
),
7380
0n,
7481
);

packages/ui/src/features/retrofunding/components/Distribute/components/DistributeTable/ProjectTableRow.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { NumericFormat } from "react-number-format";
44
import { getTransactionUrl } from "@/lib/explorer/getTransactionUrl";
55
import { cn } from "@/lib/utils";
66
import { Button, Checkbox } from "@/primitives";
7-
import { ApplicationPayout, PoolConfig } from "@/types/distribute";
87
import { TableRow, TableCell } from "@/primitives/Table";
8+
import { ApplicationPayout, PoolConfig } from "@/types/distribute";
99

10-
import { formatAmountFromPercentage } from "../../utils";
10+
import {
11+
formatAmountFromPercentageWithConstant,
12+
getAvailableTokensToDistribute,
13+
} from "../../utils";
1114

1215
interface ApplicationTableRowProps {
1316
application: ApplicationPayout;
@@ -61,6 +64,10 @@ export const ProjectTableRow = ({
6164
return () => input?.removeEventListener("wheel", handleWheel);
6265
}, [handleWheel, inputRef.current]);
6366

67+
const availableTokensToDistribute = getAvailableTokensToDistribute(
68+
allApplications.length,
69+
poolConfig,
70+
);
6471
const handleSafeChange = useCallback(
6572
(newValue: number) => {
6673
const otherApplicationsTotal = editedApplications
@@ -73,7 +80,8 @@ export const ProjectTableRow = ({
7380
);
7481

7582
const formatPayoutAmount = (percentage: number): number => {
76-
const amount = (percentage / 100) * poolConfig.amountOfTokensToDistribute;
83+
const amount =
84+
(percentage / 100) * availableTokensToDistribute + poolConfig.constantAmountPerGrant;
7785
return Number(amount.toFixed(4));
7886
};
7987

@@ -169,10 +177,11 @@ export const ProjectTableRow = ({
169177
onDistribute([
170178
{
171179
applicationId: application.id,
172-
amount: formatAmountFromPercentage(
180+
amount: formatAmountFromPercentageWithConstant(
173181
poolConfig.amountOfTokensToDistribute,
174182
application.payoutPercentage,
175183
poolConfig.tokenDecimals,
184+
poolConfig.constantAmountPerGrant,
176185
),
177186
},
178187
])

packages/ui/src/features/retrofunding/components/Distribute/components/FundRoundSection.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ export const FundRoundSection = ({
6464
<span className="font-ui-mono text-sm font-bold">
6565
{poolConfig.amountOfTokensToDistribute} {poolConfig.tokenTicker}
6666
</span>
67+
{!!poolConfig.constantAmountPerGrant && (
68+
<div className="flex items-center gap-1 pl-5">
69+
<Icon
70+
className="size-5 rounded-full text-yellow-500"
71+
type={IconType.INFORMATION_CIRCLE}
72+
/>
73+
<span className="font-ui-sans text-sm font-normal">
74+
This round has a constant distribution amount per application of
75+
</span>
76+
77+
<span className="font-ui-mono text-sm font-bold">
78+
{poolConfig.constantAmountPerGrant} {poolConfig.tokenTicker}
79+
</span>
80+
</div>
81+
)}
6782
</div>
6883
)}
6984
</div>

packages/ui/src/features/retrofunding/components/Distribute/utils.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { formatUnits, parseUnits } from "viem";
22

3+
import { PoolConfig } from "@/types/distribute";
4+
35
/**
46
* Calculates a percentage of a token amount and converts it to the token's smallest unit (wei/gwei/etc)
57
* @param amountOfTokens - The total amount of tokens in human readable format (e.g., 100 for 100 tokens)
@@ -15,6 +17,26 @@ export const formatAmountFromPercentage = (
1517
return safeParseUnits(((amountOfTokens * percentage) / 100) * 10 ** 9, tokenDecimals) / 10n ** 9n;
1618
};
1719

20+
/**
21+
* Formats a token amount from a percentage and a constant amount per grant
22+
* @param amountOfTokens - The total amount of tokens in human readable format (e.g., 100 for 100 tokens)
23+
* @param percentage - The percentage to calculate (e.g., 50 for 50%)
24+
* @param tokenDecimals - The number of decimal places for the token (e.g., 18 for ETH)
25+
* @param constantAmountPerGrant - The constant amount per grant in human readable format (e.g., 100 for 100 tokens)
26+
* @returns The formatted amount in the token's smallest unit as a bigint
27+
*/
28+
export const formatAmountFromPercentageWithConstant = (
29+
amountOfTokens: number,
30+
percentage: number,
31+
tokenDecimals: number,
32+
constantAmountPerGrant: number,
33+
) => {
34+
return (
35+
formatAmountFromPercentage(amountOfTokens, percentage, tokenDecimals) +
36+
safeParseUnits(constantAmountPerGrant.toString(), tokenDecimals)
37+
);
38+
};
39+
1840
/**
1941
* Safely parses token amounts by converting scientific notation (e.g., 1e18) to decimal format.
2042
* Resolves common parseUnits errors when dealing with scientific notation inputs.
@@ -83,3 +105,22 @@ export const formatAmount = (amount: bigint, decimals: number, maxDecimals?: num
83105
maximumSignificantDigits: decimals,
84106
});
85107
};
108+
109+
export const getAvailableTokensToDistribute = (
110+
numberOfApplications: number,
111+
poolConfig: PoolConfig,
112+
) => {
113+
const constantDistributeAmount = (
114+
numberOfApplications * poolConfig.constantAmountPerGrant
115+
).toLocaleString("fullwide", {
116+
useGrouping: false,
117+
maximumSignificantDigits: 9,
118+
});
119+
120+
const totalConstantDistributeAmount = Number(safeParseUnits(constantDistributeAmount, 9)) / 1e9;
121+
122+
const availableTokensToDistribute =
123+
poolConfig.amountOfTokensToDistribute - totalConstantDistributeAmount;
124+
125+
return availableTokensToDistribute;
126+
};

packages/ui/src/types/distribute.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface ApplicationPayout {
1111

1212
export interface PoolConfig {
1313
tokenTicker: string;
14+
constantAmountPerGrant: number;
1415
amountOfTokensInPool: bigint | string;
1516
amountOfTokensToDistribute: number;
1617
tokenDecimals: number;

0 commit comments

Comments
 (0)