Skip to content

Commit 3f8f5c4

Browse files
add Edit functionality
1 parent da19c21 commit 3f8f5c4

File tree

5 files changed

+337
-166
lines changed

5 files changed

+337
-166
lines changed

packages/hardhat/contracts/Marketplace.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ contract Marketplace is ReentrancyGuard {
2424
event ListingAction(uint256 indexed id, address indexed caller, bytes32 action);
2525
event ListingActivationChanged(uint256 indexed listingId, bool active);
2626
event ListingDeleted(uint256 indexed id);
27+
event ListingContenthashUpdated(uint256 indexed id, string contenthash);
2728

2829
function createListing(
2930
address listingType,
@@ -64,6 +65,15 @@ contract Marketplace is ReentrancyGuard {
6465
emit ListingDeleted(listingId);
6566
}
6667

68+
function setListingContenthash(uint256 listingId, string calldata newContenthash) external {
69+
ListingPointer storage record = listings[listingId];
70+
if (record.creator == address(0)) revert ListingNotFound();
71+
if (msg.sender != record.creator) revert NotListingCreator();
72+
73+
record.contenthash = newContenthash;
74+
emit ListingContenthashUpdated(listingId, newContenthash);
75+
}
76+
6777
function getListing(uint256 id) external view returns (
6878
address creator,
6979
address listingType,

packages/hardhat/contracts/SimpleListings.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ contract SimpleListings is IListingType {
3434
);
3535
event SimpleListingSold(uint256 indexed listingId, address indexed buyer, uint256 price, address paymentToken);
3636
event SimpleListingClosed(uint256 indexed listingId, address indexed caller);
37+
event SimpleListingUpdated(uint256 indexed listingId, address indexed caller, address paymentToken, uint256 price);
3738

3839
constructor(address _marketplace) {
3940
if (_marketplace == address(0)) revert MarketplaceZeroAddress();
@@ -107,6 +108,20 @@ contract SimpleListings is IListingType {
107108
emit SimpleListingClosed(listingId, caller);
108109
}
109110

111+
function update(
112+
uint256 listingId,
113+
address creator,
114+
bool,
115+
address caller,
116+
bytes calldata data
117+
) external onlySelf {
118+
if (creator != caller) revert NotCreator();
119+
(address paymentToken, uint256 price) = abi.decode(data, (address, uint256));
120+
if (price == 0) revert PriceZero();
121+
listings[listingId] = SimpleListing({ paymentToken: paymentToken, price: price });
122+
emit SimpleListingUpdated(listingId, caller, paymentToken, price);
123+
}
124+
110125
function handleAction(
111126
uint256 listingId,
112127
address creator,

packages/indexer/src/index.ts

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ async function fetchIpfsJson(cidOrUrl: string): Promise<any | null> {
8080
return null;
8181
}
8282

83+
const denormalizeMetadata = (json: any): any => {
84+
return {
85+
metadata: json,
86+
title: typeof json?.title === "string" ? json.title : null,
87+
description: typeof json?.description === "string" ? json.description : null,
88+
category: typeof json?.category === "string" ? json.category : null,
89+
image: typeof json?.image === "string" ? json.image : null,
90+
contact: typeof json?.contact === "object" ? json.contact : (typeof json?.contact === "string" ? json.contact : null),
91+
tags: Array.isArray(json?.tags) ? json.tags : (typeof json?.tags === "string" ? json.tags : null),
92+
price: typeof json?.price === "string" || typeof json?.price === "number" ? String(json.price) : null,
93+
currency: typeof json?.currency === "string" ? json.currency : null,
94+
locationId: typeof json?.locationId === "string" ? json.locationId : null,
95+
};
96+
}
97+
8398
// Minimal ABI to read listing pointer + data from Marketplace
8499
const marketplaceGetListingAbi = [
85100
{
@@ -97,6 +112,12 @@ const marketplaceGetListingAbi = [
97112
},
98113
] as const;
99114

115+
const erc20MetaAbi = [
116+
{ name: "name", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
117+
{ name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
118+
{ name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
119+
];
120+
100121
function signatureToSelector(signature: string): `0x${string}` {
101122
const hash = keccak256(stringToHex(signature));
102123
return (`0x${hash.slice(2, 10)}`) as `0x${string}`;
@@ -153,11 +174,6 @@ ponder.on("Marketplace:ListingCreated" as any, async ({ event, context }) => {
153174
.set({ tokenName: "Ether", tokenSymbol: "ETH", tokenDecimals: 18 })
154175
.where(eq(listings.id, id));
155176
} else {
156-
const erc20MetaAbi = [
157-
{ name: "name", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
158-
{ name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
159-
{ name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
160-
] as const;
161177
const [name, symbol, decimals] = await Promise.all([
162178
publicClient.readContract({ address: pt as `0x${string}`, abi: erc20MetaAbi, functionName: "name" }).catch(() => null),
163179
publicClient.readContract({ address: pt as `0x${string}`, abi: erc20MetaAbi, functionName: "symbol" }).catch(() => null),
@@ -181,19 +197,7 @@ ponder.on("Marketplace:ListingCreated" as any, async ({ event, context }) => {
181197
try {
182198
const json: any | null = await fetchIpfsJson(cid);
183199
if (json) {
184-
const denorm: any = {
185-
metadata: json,
186-
// Top-level denormalized fields for convenient querying & UI
187-
title: typeof json?.title === "string" ? json.title : null,
188-
description: typeof json?.description === "string" ? json.description : null,
189-
category: typeof json?.category === "string" ? json.category : null,
190-
image: typeof json?.image === "string" ? json.image : null,
191-
contact: typeof json?.contact === "object" ? json.contact : (typeof json?.contact === "string" ? json.contact : null),
192-
tags: Array.isArray(json?.tags) ? json.tags : (typeof json?.tags === "string" ? json.tags : null),
193-
price: typeof json?.price === "string" || typeof json?.price === "number" ? String(json.price) : null,
194-
currency: typeof json?.currency === "string" ? json.currency : null,
195-
locationId: typeof json?.locationId === "string" ? json.locationId : null,
196-
};
200+
const denorm = denormalizeMetadata(json);
197201
await db.sql.update(listings).set(denorm).where(eq(listings.id, id));
198202
}
199203
} catch {}
@@ -244,7 +248,74 @@ ponder.on("Marketplace:ListingDeleted" as any, async ({ event, context }) => {
244248
await db.sql.delete(listings).where(eq(listings.id, listingId));
245249
});
246250

247-
// (No SimpleListings event handlers required)
251+
ponder.on("Marketplace:ListingContenthashUpdated" as any, async ({ event, context }) => {
252+
const { db } = context;
253+
const listingId = (event as any).args.id.toString();
254+
const cid = (event as any).args.contenthash as string;
255+
256+
try {
257+
await db.sql.update(listings).set({ cid }).where(eq(listings.id, listingId));
258+
const json: any | null = await fetchIpfsJson(cid);
259+
if (json) {
260+
const denorm = denormalizeMetadata(json);
261+
await db.sql.update(listings).set(denorm).where(eq(listings.id, listingId));
262+
}
263+
264+
try {
265+
const mpAddress = ((event as any).log?.address as string) || ((event as any).transaction?.to as string);
266+
if (mpAddress) {
267+
const [, , , , listingData] = (await publicClient.readContract({
268+
address: mpAddress as `0x${string}`,
269+
abi: marketplaceGetListingAbi,
270+
functionName: "getListing",
271+
args: [BigInt(listingId)],
272+
})) as unknown as [string, string, string, boolean, Hex];
273+
274+
if (listingData && (listingData).length > 0) {
275+
try {
276+
const [paymentToken, price] = decodeAbiParameters(
277+
[{ type: "address" }, { type: "uint256" }],
278+
listingData,
279+
);
280+
await db.sql
281+
.update(listings)
282+
.set({
283+
paymentToken: paymentToken?.toLowerCase?.() ?? null,
284+
priceWei: price?.toString?.() ?? null,
285+
})
286+
.where(eq(listings.id, listingId));
287+
288+
const isEth = !paymentToken || paymentToken === zeroAddress;
289+
if (isEth) {
290+
await db.sql
291+
.update(listings)
292+
.set({ tokenName: "Ether", tokenSymbol: "ETH", tokenDecimals: 18 })
293+
.where(eq(listings.id, listingId));
294+
} else {
295+
const [name, symbol, decimals] = await Promise.all([
296+
publicClient.readContract({ address: paymentToken as `0x${string}`, abi: erc20MetaAbi, functionName: "name" }),
297+
publicClient.readContract({ address: paymentToken as `0x${string}`, abi: erc20MetaAbi, functionName: "symbol" }),
298+
publicClient.readContract({ address: paymentToken as `0x${string}`, abi: erc20MetaAbi, functionName: "decimals" }),
299+
]);
300+
await db.sql
301+
.update(listings)
302+
.set({
303+
tokenName: typeof name === "string" ? name : null,
304+
tokenSymbol: typeof symbol === "string" ? symbol : null,
305+
tokenDecimals: typeof decimals === "number" ? decimals : (typeof decimals === "bigint" ? Number(decimals) : null),
306+
})
307+
.where(eq(listings.id, listingId));
308+
}
309+
} catch {}
310+
}
311+
}
312+
} catch (chainErr) {
313+
console.error(`[Ponder] Error refreshing on-chain listing data for ${listingId}:`, chainErr);
314+
}
315+
} catch (e) {
316+
console.error(`[Ponder] Error handling ListingContenthashUpdated for ${listingId}:`, e);
317+
}
318+
});
248319

249320
// --- EAS Reviews indexing ---
250321
// Read new per-chain keyed shape only

packages/nextjs/app/listing/[id]/page.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ const ListingDetailsPageInner = () => {
164164
}
165165
}, [idNum, writeMarketplace, router]);
166166

167+
const handleOpenEdit = useCallback(() => {
168+
const idStr = params?.id;
169+
if (!idStr) return;
170+
const base = `/listing/new?edit=${encodeURIComponent(idStr)}`;
171+
const from = indexed?.locationId;
172+
router.push(from ? `${base}&loc=${encodeURIComponent(from)}` : base);
173+
}, [params?.id, indexed?.locationId, router]);
174+
167175
const tags = useMemo(() => {
168176
const raw = (data?.tags ?? (indexed as any)?.tags) as unknown;
169177
if (!raw) return [] as string[];
@@ -287,13 +295,23 @@ const ListingDetailsPageInner = () => {
287295
<div className="flex items-center gap-2 ml-auto">
288296
<div className={`badge ${active ? "badge-success" : ""}`}>{active ? "Active" : "Sold"}</div>
289297
{isCreator && (
290-
<button
291-
className="badge badge-error cursor-pointer hover:opacity-80 transition-opacity"
292-
onClick={handleDelete}
293-
disabled={deleting}
294-
>
295-
{deleting ? "Deleting..." : "Delete"}
296-
</button>
298+
<div className="dropdown dropdown-end">
299+
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm text-lg">
300+
301+
</div>
302+
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-[1] w-32 p-2 shadow">
303+
<li>
304+
<button onClick={handleOpenEdit} className="w-full text-left">
305+
Edit
306+
</button>
307+
</li>
308+
<li>
309+
<button onClick={handleDelete} disabled={deleting} className="w-full text-left text-error">
310+
{deleting ? "Deleting..." : "Delete"}
311+
</button>
312+
</li>
313+
</ul>
314+
</div>
297315
)}
298316
</div>
299317
</div>

0 commit comments

Comments
 (0)