Skip to content

Commit a9fa7b1

Browse files
committed
feat: enforce v2 poap event gating and chain support
1 parent d0e43dc commit a9fa7b1

21 files changed

+969
-435
lines changed

client/__tests__/CreateFormIntegration.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ const CreateFormLogic = () => {
144144
};
145145

146146
describe("Create Form Integration", () => {
147-
const user = userEvent.setup();
147+
let user: ReturnType<typeof userEvent.setup>;
148+
149+
beforeEach(() => {
150+
user = userEvent.setup();
151+
});
148152

149153
it("validates complete form correctly", async () => {
150154
render(<CreateFormLogic />);

client/__tests__/MetadataParser.test.tsx

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ describe("parseMetadata", () => {
7474

7575
render(<MetadataTest metadata={metadata} />);
7676
expect(screen.getByTestId("name")).toHaveTextContent("My Special Jar");
77-
expect(screen.getByTestId("description")).toHaveTextContent(
78-
"A detailed description",
79-
);
80-
expect(screen.getByTestId("image")).toHaveAttribute(
81-
"src",
82-
"https://example.com/image.png",
83-
);
77+
expect(screen.getByTestId("description")).toHaveTextContent(
78+
"A detailed description",
79+
);
80+
expect(screen.getByTestId("image")).toHaveAttribute(
81+
"src",
82+
expect.stringContaining(
83+
encodeURIComponent("https://example.com/image.png"),
84+
),
85+
);
8486
expect(screen.getByTestId("link")).toHaveAttribute(
8587
"href",
8688
"https://example.com/project",
@@ -96,11 +98,13 @@ describe("parseMetadata", () => {
9698

9799
render(<MetadataTest metadata={metadata} />);
98100
expect(screen.getByTestId("name")).toHaveTextContent("Partial Jar");
99-
expect(screen.getByTestId("description")).toHaveTextContent(metadata); // fallback to raw string
100-
expect(screen.getByTestId("image")).toHaveAttribute(
101-
"src",
102-
"https://example.com/image.png",
103-
);
101+
expect(screen.getByTestId("description")).toHaveTextContent(metadata); // fallback to raw string
102+
expect(screen.getByTestId("image")).toHaveAttribute(
103+
"src",
104+
expect.stringContaining(
105+
encodeURIComponent("https://example.com/image.png"),
106+
),
107+
);
104108
expect(screen.getByTestId("link")).toHaveAttribute("href", "");
105109
});
106110

@@ -150,13 +154,15 @@ describe("parseMetadata", () => {
150154

151155
render(<MetadataTest metadata={metadata} />);
152156
expect(screen.getByTestId("name")).toHaveTextContent("Complete Jar");
153-
expect(screen.getByTestId("description")).toHaveTextContent(
154-
"Full description",
155-
);
156-
expect(screen.getByTestId("image")).toHaveAttribute(
157-
"src",
158-
"https://example.com/complete.png",
159-
);
157+
expect(screen.getByTestId("description")).toHaveTextContent(
158+
"Full description",
159+
);
160+
expect(screen.getByTestId("image")).toHaveAttribute(
161+
"src",
162+
expect.stringContaining(
163+
encodeURIComponent("https://example.com/complete.png"),
164+
),
165+
);
160166
expect(screen.getByTestId("link")).toHaveAttribute(
161167
"href",
162168
"https://complete-project.com",
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { decodeFunctionData, encodeFunctionData } from "viem";
2+
import { describe, expect, it, vi } from "vitest";
3+
import { cookieJarFactoryAbi } from "@/generated";
4+
import {
5+
FACTORY_DEFAULT_FEE_SENTINEL,
6+
buildV2CreateCookieJarArgs,
7+
getFeePercentageOnDeposit,
8+
} from "@/hooks/jar/createV2CreateArgs";
9+
import { ETH_ADDRESS, HATS_PROTOCOL_ADDRESS, POAP_TOKEN_ADDRESS } from "@/lib/blockchain/constants";
10+
11+
vi.mock("@/hooks/jar/schemas/jarCreationSchema", () => ({
12+
AccessType: {
13+
Allowlist: 0,
14+
NFTGated: 1,
15+
POAP: 2,
16+
Unlock: 3,
17+
Hypercert: 4,
18+
Hats: 5,
19+
},
20+
NFTType: {
21+
None: 0,
22+
ERC721: 1,
23+
ERC1155: 2,
24+
},
25+
}));
26+
27+
const AccessType = {
28+
Allowlist: 0,
29+
NFTGated: 1,
30+
POAP: 2,
31+
Unlock: 3,
32+
Hypercert: 4,
33+
Hats: 5,
34+
} as const;
35+
36+
const NFTType = {
37+
None: 0,
38+
ERC721: 1,
39+
ERC1155: 2,
40+
} as const;
41+
42+
type JarCreationFormData = Parameters<typeof buildV2CreateCookieJarArgs>[0]["values"];
43+
type ProtocolConfig = JarCreationFormData["protocolConfig"];
44+
45+
type MakeValuesOverrides = Partial<Omit<JarCreationFormData, "protocolConfig">> & {
46+
protocolConfig?: Partial<ProtocolConfig>;
47+
};
48+
49+
function makeValues(overrides: MakeValuesOverrides = {}): JarCreationFormData {
50+
const baseValues: JarCreationFormData = {
51+
jarName: "Test Jar",
52+
jarOwnerAddress: "0x1234567890123456789012345678901234567890",
53+
supportedCurrency: ETH_ADDRESS,
54+
metadata: "Test metadata",
55+
imageUrl: "",
56+
externalLink: "",
57+
showCustomCurrency: false,
58+
customCurrencyAddress: "",
59+
withdrawalOption: 0,
60+
fixedAmount: "1",
61+
maxWithdrawal: "2",
62+
withdrawalInterval: "7",
63+
strictPurpose: true,
64+
emergencyWithdrawalEnabled: true,
65+
oneTimeWithdrawal: false,
66+
accessType: AccessType.Allowlist,
67+
nftAddresses: [] as string[],
68+
nftTypes: [] as number[],
69+
protocolConfig: { accessType: "Allowlist" },
70+
enableCustomFee: false,
71+
customFee: "",
72+
streamingEnabled: false,
73+
requireStreamApproval: true,
74+
maxStreamRate: "1.0",
75+
minStreamDuration: "1",
76+
autoSwapEnabled: false,
77+
};
78+
79+
return {
80+
...baseValues,
81+
...overrides,
82+
protocolConfig: {
83+
...baseValues.protocolConfig,
84+
...overrides.protocolConfig,
85+
},
86+
};
87+
}
88+
89+
describe("buildV2CreateCookieJarArgs", () => {
90+
it("encodes createCookieJar args compatible with current factory ABI", () => {
91+
const args = buildV2CreateCookieJarArgs({
92+
values: makeValues(),
93+
metadata: "metadata",
94+
parseAmount: (amount) => BigInt(Math.floor(Number.parseFloat(amount) * 1e18)),
95+
});
96+
97+
const data = encodeFunctionData({
98+
abi: cookieJarFactoryAbi,
99+
functionName: "createCookieJar",
100+
args,
101+
});
102+
expect(data.startsWith("0x")).toBe(true);
103+
104+
const decoded = decodeFunctionData({
105+
abi: cookieJarFactoryAbi,
106+
data,
107+
});
108+
expect(decoded.functionName).toBe("createCookieJar");
109+
expect(decoded.args?.length).toBe(3);
110+
});
111+
112+
it("uses default fee sentinel when custom fee is disabled", () => {
113+
const fee = getFeePercentageOnDeposit(makeValues({ enableCustomFee: false }));
114+
expect(fee).toBe(FACTORY_DEFAULT_FEE_SENTINEL);
115+
});
116+
117+
it("uses explicit custom fee when provided", () => {
118+
const fee = getFeePercentageOnDeposit(
119+
makeValues({ enableCustomFee: true, customFee: "2.5" }),
120+
);
121+
expect(fee).toBe(250n);
122+
});
123+
124+
it("supports explicit zero-percent fee", () => {
125+
const fee = getFeePercentageOnDeposit(
126+
makeValues({ enableCustomFee: true, customFee: "0" }),
127+
);
128+
expect(fee).toBe(0n);
129+
});
130+
131+
it("maps NFT ERC721 access to contract enum ERC721", () => {
132+
const [jarConfig, accessConfig] = buildV2CreateCookieJarArgs({
133+
values: makeValues({
134+
accessType: AccessType.NFTGated,
135+
nftAddresses: ["0x1111111111111111111111111111111111111111"],
136+
nftTypes: [NFTType.ERC721],
137+
}),
138+
metadata: "metadata",
139+
parseAmount: () => 1n,
140+
});
141+
expect(jarConfig.accessType).toBe(1);
142+
expect(accessConfig.nftRequirement.nftContract).toBe(
143+
"0x1111111111111111111111111111111111111111",
144+
);
145+
});
146+
147+
it("maps NFT ERC1155 access to contract enum ERC1155", () => {
148+
const [jarConfig] = buildV2CreateCookieJarArgs({
149+
values: makeValues({
150+
accessType: AccessType.NFTGated,
151+
nftAddresses: ["0x1111111111111111111111111111111111111111"],
152+
nftTypes: [NFTType.ERC1155],
153+
}),
154+
metadata: "metadata",
155+
parseAmount: () => 1n,
156+
});
157+
expect(jarConfig.accessType).toBe(2);
158+
});
159+
160+
it("maps POAP, Unlock, Hypercert, and Hats to supported contract enum domain", () => {
161+
const [poapJar, poapAccess] = buildV2CreateCookieJarArgs({
162+
values: makeValues({
163+
accessType: AccessType.POAP,
164+
protocolConfig: { accessType: "POAP", eventId: "1234" },
165+
}),
166+
metadata: "metadata",
167+
parseAmount: () => 1n,
168+
});
169+
expect(poapJar.accessType).toBe(1);
170+
expect(poapAccess.nftRequirement.nftContract).toBe(POAP_TOKEN_ADDRESS);
171+
expect(poapAccess.nftRequirement.tokenId).toBe(1234n);
172+
expect(poapAccess.nftRequirement.minBalance).toBe(1n);
173+
174+
const [unlockJar] = buildV2CreateCookieJarArgs({
175+
values: makeValues({
176+
accessType: AccessType.Unlock,
177+
protocolConfig: {
178+
accessType: "Unlock",
179+
unlockAddress: "0x2222222222222222222222222222222222222222",
180+
},
181+
}),
182+
metadata: "metadata",
183+
parseAmount: () => 1n,
184+
});
185+
expect(unlockJar.accessType).toBe(1);
186+
187+
const [hypercertJar] = buildV2CreateCookieJarArgs({
188+
values: makeValues({
189+
accessType: AccessType.Hypercert,
190+
protocolConfig: {
191+
accessType: "Hypercert",
192+
hypercertAddress: "0x3333333333333333333333333333333333333333",
193+
hypercertTokenId: "42",
194+
hypercertMinBalance: 5,
195+
},
196+
}),
197+
metadata: "metadata",
198+
parseAmount: () => 1n,
199+
});
200+
expect(hypercertJar.accessType).toBe(2);
201+
202+
const [hatsJar, hatsAccess] = buildV2CreateCookieJarArgs({
203+
values: makeValues({
204+
accessType: AccessType.Hats,
205+
protocolConfig: { accessType: "Hats", hatsId: 99 },
206+
}),
207+
metadata: "metadata",
208+
parseAmount: () => 1n,
209+
});
210+
expect(hatsJar.accessType).toBe(2);
211+
expect(hatsAccess.nftRequirement.nftContract).toBe(HATS_PROTOCOL_ADDRESS);
212+
});
213+
});

client/__tests__/hooks/useJarCreation.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ vi.mock("@/config/deployments.auto", () => ({
1818
},
1919
}));
2020

21-
import { useJarCreation } from "@/hooks/jar/useJarCreation";
2221
import { ETH_ADDRESS } from "@/lib/blockchain/constants";
2322

2423
// Mock wagmi hooks
@@ -73,6 +72,12 @@ const describeOrSkip =
7372

7473
describeOrSkip("useJarCreation", () => {
7574
let queryClient: QueryClient;
75+
let useJarCreation: typeof import("@/hooks/jar/useJarCreation").useJarCreation;
76+
77+
beforeAll(async () => {
78+
const module = await import("@/hooks/jar/useJarCreation");
79+
useJarCreation = module.useJarCreation;
80+
});
7681

7782
beforeEach(() => {
7883
vi.clearAllMocks();
@@ -85,6 +90,9 @@ describeOrSkip("useJarCreation", () => {
8590
});
8691

8792
const renderHookWithProviders = () => {
93+
if (!useJarCreation) {
94+
throw new Error("useJarCreation hook not loaded");
95+
}
8896
return renderHook(() => useJarCreation(), {
8997
wrapper: ({ children }: { children: React.ReactNode }) =>
9098
React.createElement(

0 commit comments

Comments
 (0)