Skip to content

Commit 13e2227

Browse files
Enhance Marketplace Mini App with detailed README, refactor contract interfaces, and improve listing lifecycle management. Introduce new event emissions and modularize listing actions for better clarity and functionality. Update deployment scripts for EAS review schema configuration.
1 parent 0778480 commit 13e2227

File tree

5 files changed

+252
-221
lines changed

5 files changed

+252
-221
lines changed

README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,92 @@
1+
# Marketplace Mini App
2+
3+
Farcaster-ready marketplace mini app built on Scaffold-ETH 2. It lets creators publish fixed-price listings payable in ETH or ERC-20, buyers purchase them trustlessly, and both parties leave on-chain EAS reviews. The app ships with a modular contract design, a Next.js frontend wired to SE-2 hooks, and optional indexing via Ponder.
4+
5+
## What’s inside
6+
- **Frontend**: Next.js (App Router), Wagmi/Viem, RainbowKit, SE-2 components and hooks. Farcaster Mini App integration (Quick Auth, splash handling).
7+
- **Contracts**: `Marketplace`, `IListingType`, `SimpleListings`, and local `TestERC20` tokens for dev.
8+
- **Reviews**: EAS review schema registered automatically on local and known networks; config is emitted for frontend and indexer.
9+
- **Indexer**: Ponder project to index marketplace and review activity.
10+
11+
## Farcaster Mini App integration
12+
- The mini app calls `sdk.actions.ready()` after the app is mounted to dismiss the splash screen (see `packages/nextjs/components/MiniappProvider.tsx`).
13+
- Authenticated requests can be made with `sdk.quickAuth.fetch` or by pulling a token via `sdk.quickAuth.getToken` on the client and sending it to your backend. See Farcaster Mini App docs for validation on the server using `@farcaster/quick-auth`.
14+
15+
## Contracts architecture
16+
17+
### Roles and components
18+
- **Marketplace.sol**: Router/orchestrator that stores pointers to listings and delegates all lifecycle operations to a pluggable listing-type contract implementing `IListingType`.
19+
- **IListingType.sol**: Lifecycle interface that any listing-type must implement. Phases: create, optional pre-buy, sale, and admin close; each has `before`, `on`, and `after` hooks.
20+
- **SimpleListings.sol**: A concrete `IListingType` implementation for fixed-price listings payable in ETH or an ERC-20.
21+
- **TestERC20.sol**: Simple mintable tokens for local development (2 and 6 decimals). Only deployed on `localhost`/`hardhat`.
22+
23+
### Marketplace data model
24+
- `listingCount`: sequential marketplace-level IDs starting at 1.
25+
- `listings[id]`: `ListingPointer { creator, listingType, listingId }` where:
26+
- `listingType` is the address of the listing-type contract (e.g., `SimpleListings`).
27+
- `listingId` is the inner ID returned by the listing-type on creation.
28+
29+
### Lifecycle and how contracts interact
30+
1) **Create**: `Marketplace.createListing(listingType, data)`
31+
- Calls `beforeCreate(data)` on the `listingType`.
32+
- Calls `onCreate(msg.sender, data)` which returns `innerId` (must be non-zero).
33+
- Stores `ListingPointer` under new marketplace ID and calls `afterCreate`.
34+
- Emits `ListingCreated(id, creator, listingType, innerId)`.
35+
36+
2) **Optional pre-buy step**: `Marketplace.preBuyAction(id, data)`
37+
- Delegates to `beforePreBuy / onPreBuy / afterPreBuy` on the `listingType` and emits `ListingPreBuy`.
38+
- Useful for escrows, auctions, or reservations. `SimpleListings` treats this as a no-op.
39+
40+
3) **Sale**: `Marketplace.buyListing(id, data)`
41+
- Calls `beforeSale / onSale / afterSale` on the `listingType`, then emits `ListingSold(id, buyer)`.
42+
43+
4) **Close**: `Marketplace.closeListing(id, data)`
44+
- Only the marketplace-level `creator` can close. Delegates to `beforeClose / onClose / afterClose` and emits `ListingClosed`.
45+
46+
### SimpleListings data and behavior
47+
- Stored as `SimpleListing { creator, paymentToken, price, ipfsHash, active }`.
48+
- `paymentToken == address(0)`: buyer pays in ETH by sending exactly `price` wei. ETH is forwarded directly to the creator.
49+
- `paymentToken != address(0)`: buyer pays in ERC-20; must approve `SimpleListings` for `price` before calling `buyListing`. No ETH is accepted in this path.
50+
- On successful sale, an event `SimpleListingSold(listingId, buyer, price, paymentToken)` is emitted and the listing is set inactive.
51+
- Only the listing `creator` can close an active listing via the marketplace; closing marks it inactive and emits `SimpleListingClosed`.
52+
53+
### Encoding and views
54+
- `createListing(listingType, data)` forwards `data` to the listing-type. For `SimpleListings`, `data` must be `abi.encode(address paymentToken, uint256 price, string ipfsHash)`.
55+
- `getListing(id)` returns `(ListingPointer pointer, bytes data)`, where `data` is `listingType.getListing(pointer.listingId)`.
56+
- For `SimpleListings.getListing(listingId)`, `data` is `abi.encode(creator, paymentToken, price, ipfsHash, active)`.
57+
58+
### Events
59+
- Marketplace: `ListingCreated`, `ListingPreBuy`, `ListingSold`, `ListingClosed`.
60+
- SimpleListings: `SimpleListingCreated`, `SimpleListingSold`, `SimpleListingClosed`.
61+
62+
## EAS Reviews
63+
- A review schema `uint256 listingId,uint8 rating,string commentIPFSHash` is registered by `packages/hardhat/deploy/05_register_review_schema.ts` on local and known networks.
64+
- After deployment, config is written to:
65+
- Frontend: `packages/nextjs/contracts/easConfig.json`
66+
- Indexer: `packages/indexer/src/easConfig.json`
67+
- Use these addresses/UIDs from your app or indexer to submit and query review attestations.
68+
69+
## Local development
70+
1. Start a local chain:
71+
- `yarn chain`
72+
2. Deploy contracts (Marketplace, SimpleListings):
73+
- `yarn deploy`
74+
3. (Optional) Deploy local EAS core + register review schema:
75+
- `yarn deploy --tags EASLocal,ReviewSchema`
76+
4. (Optional) Deploy local test tokens:
77+
- `yarn deploy --tags TestERC20`
78+
5. Start the frontend:
79+
- `yarn start``http://localhost:3000`
80+
81+
Contract artifacts/addresses are available under `packages/hardhat/deployments/<network>/`. The frontend uses SE-2’s generated `deployedContracts.ts` wiring for reads/writes.
82+
83+
## Frontend contract interactions (SE-2 way)
84+
- Reads: `useScaffoldReadContract({ contractName, functionName, args })`
85+
- Writes: `useScaffoldWriteContract({ contractName })``writeContractAsync({ functionName, args, value? })`
86+
- Events: `useScaffoldEventHistory({ contractName, eventName, watch? })`
87+
88+
---
89+
190
# 🏗 Scaffold-ETH 2
291

392
<h4 align="center">
@@ -77,4 +166,4 @@ To know more about its features, check out our [website](https://scaffoldeth.io)
77166

78167
We welcome contributions to Scaffold-ETH 2!
79168

80-
Please see [CONTRIBUTING.MD](https://github.com/scaffold-eth/scaffold-eth-2/blob/main/CONTRIBUTING.md) for more information and guidelines for contributing to Scaffold-ETH 2.
169+
Please see [CONTRIBUTING.MD](https://github.com/scaffold-eth/scaffold-eth-2/blob/main/CONTRIBUTING.md) for more information and guidelines for contributing to Scaffold-ETH 2.

packages/hardhat/contracts/IListingType.sol

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,43 @@
22
pragma solidity >=0.8.0 <0.9.0;
33

44
interface IListingType {
5-
// Creation lifecycle
6-
function beforeCreate(bytes calldata data) external returns (bool);
7-
function onCreate(address creator, bytes calldata data) external returns (uint256 listingId);
8-
function afterCreate(uint256 listingId, bytes calldata data) external returns (bool);
5+
struct ListingTypeMetadata {
6+
string name; // e.g. "SimpleListing"
7+
string version; // e.g. "1.0.0"
8+
string description; // optional
9+
string abi; // JSON ABI encoded as a string
10+
}
911

10-
// Sale lifecycle
11-
function beforeSale(uint256 listingId, address buyer, bytes calldata data) external returns (bool);
12-
function onSale(uint256 listingId, address buyer, bytes calldata data) external payable returns (bool);
13-
function afterSale(uint256 listingId, address buyer, bytes calldata data) external returns (bool);
12+
/// @notice Creates a new listing
13+
/// @param creator The creator of the listing
14+
/// @param listingId The ID of the listing
15+
/// @param data The data for the listing
16+
/// @dev All listings start at this step
17+
function create(address creator, uint256 listingId, bytes calldata data)
18+
external
19+
returns (bool success);
1420

15-
// Pre-buy lifecycle (e.g., escrow setup, pre-payment, auction mechanism etc.)
16-
function beforePreBuy(uint256 listingId, address buyer, bytes calldata data) external returns (bool);
17-
function onPreBuy(uint256 listingId, address buyer, bytes calldata data) external payable returns (bool);
18-
function afterPreBuy(uint256 listingId, address buyer, bytes calldata data) external returns (bool);
21+
/// @notice Handles an action for a listing
22+
/// @param listingId The ID of the listing
23+
/// @param creator The creator of the listing
24+
/// @param active Whether the listing is active
25+
/// @param caller The caller of the action
26+
/// @param action The action to handle
27+
/// @param data The data for the action
28+
/// @dev The caller must be the marketplace contract
29+
function handleAction(
30+
uint256 listingId,
31+
address creator,
32+
bool active,
33+
address caller,
34+
bytes32 action,
35+
bytes calldata data
36+
) external payable;
1937

20-
// Admin lifecycle
21-
function beforeClose(uint256 listingId, address caller, bytes calldata data) external returns (bool);
22-
function onClose(uint256 listingId, address caller, bytes calldata data) external returns (bool);
23-
function afterClose(uint256 listingId, address caller, bytes calldata data) external returns (bool);
24-
25-
// View helpers
26-
function getListing(uint256 listingId) external view returns (bytes memory data);
38+
/// @notice Returns the data for a listing
39+
/// @dev The data is the encoded data for the listing
40+
function getListing(uint256 listingId)
41+
external
42+
view
43+
returns (bytes memory data);
2744
}
Lines changed: 44 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,69 @@
1-
//SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: MIT
22
pragma solidity >=0.8.0 <0.9.0;
33

44
import { IListingType } from "./IListingType.sol";
55

66
contract Marketplace {
7-
// Custom errors
8-
error ListingTypeZeroAddress();
7+
error ListingCreationFailed();
8+
error OnlyListingTypeCanModify();
99
error ListingNotFound();
10-
error NotCreator();
11-
error BeforeCreateFailed();
12-
error InnerIdZero();
13-
error AfterCreateFailed();
14-
error BeforePreBuyFailed();
15-
error OnPreBuyFailed();
16-
error AfterPreBuyFailed();
17-
error BeforeSaleFailed();
18-
error OnSaleFailed();
19-
error AfterSaleFailed();
20-
error BeforeCloseFailed();
21-
error OnCloseFailed();
22-
error AfterCloseFailed();
10+
2311
struct ListingPointer {
2412
address creator;
25-
address listingType; // contract implementing IListingType
26-
uint256 listingId; // ID inside the listing type contract
13+
address listingType;
14+
bytes32 contenthash;
15+
bool active;
2716
}
2817

2918
uint256 public listingCount;
3019
mapping(uint256 => ListingPointer) public listings;
3120

32-
event ListingCreated(uint256 indexed id, address indexed creator, address indexed listingType, uint256 listingId);
33-
event ListingSold(uint256 indexed id, address indexed buyer);
34-
event ListingClosed(uint256 indexed id, address indexed caller);
35-
event ListingPreBuy(uint256 indexed id, address indexed buyer);
36-
37-
modifier onlyListing(uint256 id) {
38-
if (listings[id].listingType == address(0)) revert ListingNotFound();
39-
_;
40-
}
41-
42-
function createListing(address listingType, bytes calldata data) external returns (uint256 id) {
43-
if (listingType == address(0)) revert ListingTypeZeroAddress();
44-
45-
// lifecycle: beforeCreate -> onCreate -> afterCreate
46-
if (!IListingType(listingType).beforeCreate(data)) revert BeforeCreateFailed();
47-
uint256 innerId = IListingType(listingType).onCreate(msg.sender, data);
48-
if (innerId == 0) revert InnerIdZero();
21+
event ListingCreated(uint256 indexed id, address indexed creator, address indexed listingType, uint256 listingId, bytes32 contenthash);
22+
event ListingAction(uint256 indexed id, address indexed caller, bytes32 action);
23+
event ListingActivationChanged(uint256 indexed listingId, bool active);
4924

25+
function createListing(
26+
address listingType,
27+
bytes32 contenthash,
28+
bytes calldata data
29+
) external returns (uint256 id) {
5030
id = ++listingCount;
51-
listings[id] = ListingPointer({ creator: msg.sender, listingType: listingType, listingId: innerId });
52-
53-
if (!IListingType(listingType).afterCreate(innerId, data)) revert AfterCreateFailed();
54-
emit ListingCreated(id, msg.sender, listingType, innerId);
31+
bool success = IListingType(listingType).create(msg.sender, id, data);
32+
if (!success) revert ListingCreationFailed();
33+
listings[id] = ListingPointer(msg.sender, listingType, contenthash, true);
34+
emit ListingCreated(id, msg.sender, listingType, id, contenthash);
5535
}
5636

57-
function preBuyAction(uint256 id, bytes calldata data) external payable onlyListing(id) {
37+
function callAction(
38+
uint256 id,
39+
bytes32 action,
40+
bytes calldata data
41+
) external payable {
42+
if (id > listingCount) revert ListingNotFound();
5843
ListingPointer memory ptr = listings[id];
59-
60-
if (!IListingType(ptr.listingType).beforePreBuy(ptr.listingId, msg.sender, data)) revert BeforePreBuyFailed();
61-
if (!IListingType(ptr.listingType).onPreBuy{ value: msg.value }(ptr.listingId, msg.sender, data))
62-
revert OnPreBuyFailed();
63-
if (!IListingType(ptr.listingType).afterPreBuy(ptr.listingId, msg.sender, data)) revert AfterPreBuyFailed();
64-
65-
emit ListingPreBuy(id, msg.sender);
44+
IListingType(ptr.listingType).handleAction{value: msg.value}(id, ptr.creator, ptr.active, msg.sender, action, data);
45+
emit ListingAction(id, msg.sender, action);
6646
}
6747

68-
function buyListing(uint256 id, bytes calldata data) external payable onlyListing(id) {
69-
ListingPointer memory ptr = listings[id];
70-
71-
// lifecycle: beforeSale -> onSale -> afterSale
72-
if (!IListingType(ptr.listingType).beforeSale(ptr.listingId, msg.sender, data)) revert BeforeSaleFailed();
73-
if (!IListingType(ptr.listingType).onSale{ value: msg.value }(ptr.listingId, msg.sender, data))
74-
revert OnSaleFailed();
75-
if (!IListingType(ptr.listingType).afterSale(ptr.listingId, msg.sender, data)) revert AfterSaleFailed();
76-
77-
emit ListingSold(id, msg.sender);
48+
function setActive(uint256 listingId, bool active) external {
49+
ListingPointer storage record = listings[listingId];
50+
if (msg.sender != record.listingType) revert OnlyListingTypeCanModify();
51+
record.active = active;
52+
emit ListingActivationChanged(listingId, active);
7853
}
7954

80-
function closeListing(uint256 id, bytes calldata data) external onlyListing(id) {
55+
function getListing(uint256 id) external view returns (
56+
address creator,
57+
address listingType,
58+
bytes32 contenthash,
59+
bool active,
60+
bytes memory listingData
61+
) {
8162
ListingPointer memory ptr = listings[id];
82-
if (ptr.creator != msg.sender) revert NotCreator();
83-
84-
if (!IListingType(ptr.listingType).beforeClose(ptr.listingId, msg.sender, data)) revert BeforeCloseFailed();
85-
if (!IListingType(ptr.listingType).onClose(ptr.listingId, msg.sender, data)) revert OnCloseFailed();
86-
if (!IListingType(ptr.listingType).afterClose(ptr.listingId, msg.sender, data)) revert AfterCloseFailed();
87-
emit ListingClosed(id, msg.sender);
88-
}
89-
90-
function getListing(
91-
uint256 id
92-
) external view onlyListing(id) returns (ListingPointer memory pointer, bytes memory data) {
93-
pointer = listings[id];
94-
data = IListingType(pointer.listingType).getListing(pointer.listingId);
63+
creator = ptr.creator;
64+
listingType = ptr.listingType;
65+
contenthash = ptr.contenthash;
66+
active = ptr.active;
67+
listingData = IListingType(ptr.listingType).getListing(id);
9568
}
9669
}

0 commit comments

Comments
 (0)