Skip to content

Commit ae85cca

Browse files
docs: add documentation for buyer-initiated offers and createOfferAnd… (#1017)
* docs: add documentation for buyer-initiated offers and createOfferAndCommit method * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 851ea80 commit ae85cca

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Synthesis: Buyer-Initiated Offers — `core-sdk-offers.test.ts`
2+
3+
## Overview
4+
5+
In a **buyer-initiated offer**, the buyer creates the offer on-chain; the seller is the one who "commits" to it (i.e. accepts the deal). This is the reverse of the standard flow.
6+
7+
Key constraint: `quantityAvailable` **must be 1** — buyer-initiated offers are always single-unit.
8+
9+
---
10+
11+
## Method Involved
12+
13+
`sellerCoreSDK.commitToBuyerOffer(offerId, sellerParams?)` — the seller-side equivalent of `commitToOffer`. Takes an optional `sellerParams` to customise collection, mutualizer, and royalty info at commit time.
14+
15+
The symmetric error guards are also tested:
16+
- `commitToOffer()` (seller-offer method) called on a buyer-initiated offer → `"Offer with id X is not seller initiated"`
17+
- `commitToBuyerOffer()` (buyer-offer method) called on a seller-initiated offer → `"Offer with id X is not buyer initiated"`
18+
19+
---
20+
21+
## Prerequisites
22+
23+
### 1. Dispute resolver with a non-zero fee — created once (`beforeAll`)
24+
A dedicated dispute resolver is created via `createDisputeResolver()` with:
25+
- A fee of `0.0123 ETH` on the native token (`AddressZero`)
26+
- An open seller allow-list (`[]`)
27+
28+
This DR is shared across all tests in the suite.
29+
30+
### 2. Fresh buyer + seller SDKs — created per test (`beforeEach`)
31+
`initSellerAndBuyerSDKs(seedWallet)` produces two independent funded wallets and `CoreSDK` instances.
32+
33+
### 3. Buyer-initiated offer already on-chain (`beforeEach`)
34+
`createOffer(buyerCoreSDK, { creator: OfferCreator.Buyer, quantityAvailable: 1, disputeResolverId, exchangeToken })` creates the offer as the buyer. The resulting `buyerInitiatedOffer` has:
35+
- `offer.buyer` set (buyer's account)
36+
- `offer.seller` **not set** yet (seller is assigned at commit time)
37+
- `offer.creator === OfferCreator.Buyer`
38+
39+
### 4. Seller account registered (`beforeEach`)
40+
`createSeller(sellerCoreSDK, sellerWallet.address)` — mandatory before committing.
41+
42+
### 5. Both parties deposit funds (`beforeEach`)
43+
- **Buyer** deposits the offer price into their buyer account (`buyerCoreSDK.depositFunds(buyerInitiatedOffer.buyerId, price, token)`)
44+
- **Seller** deposits the DR fee amount (`sellerCoreSDK.depositFunds(seller.id, drFeeAmount, token)`)
45+
46+
Both deposits are moved to escrow when the seller commits.
47+
48+
---
49+
50+
## Test Cases
51+
52+
### Happy path — basic commit
53+
54+
**"Seller commits to the offer, no seller param"**
55+
56+
The seller calls `commitToBuyerOffer(offerId)` with no extra params.
57+
58+
Verifications:
59+
- Before commit: buyer's available funds = offer price; seller's available funds = DR fee
60+
- After commit:
61+
- `exchange.state === ExchangeState.COMMITTED`
62+
- `exchange.seller.id` === seller's id; `exchange.buyer.id` === buyer's id
63+
- Buyer's available funds drop to `"0"` (moved to escrow)
64+
- Seller's available funds drop to `"0"` (moved to escrow)
65+
- `offer.seller` is now assigned to the committing seller
66+
67+
---
68+
69+
### Happy path — optional `sellerParams`
70+
71+
Three tests cover the optional parameters the seller can pass at commit time:
72+
73+
| Test | `sellerParams` field | What is verified |
74+
|---|---|---|
75+
| "set offer collection" | `{ collectionIndex: 1 }` | `exchange.offer.collectionIndex === "1"` and collection's `externalId` matches |
76+
| "set mutualizerAddress" | `{ mutualizerAddress }` | `exchange.mutualizerAddress` matches the mutualizer contract address |
77+
| "set royaltyInfo" | `{ royaltyInfo: { recipients, bps } }` | `exchange.offer.royaltyInfos[0]` contains the expected recipient + bps |
78+
79+
For the mutualizer test, an agreement must be created and its premium paid before committing (`newAgreementToDRFeeMutualizer` + `payPremiumToDRFeeMutualizer`).
80+
81+
For the collection test, the collection must be created first (`sellerCoreSDK.createNewCollection({ collectionId, contractUri })`).
82+
83+
---
84+
85+
### Error cases
86+
87+
| Test | Scenario | Error message |
88+
|---|---|---|
89+
| "commitToOffer() fails on buyer initiated offer" | Wrong commit method used on a buyer-initiated offer | `"Offer with id X is not seller initiated"` |
90+
| "commitToBuyerOffer() fails on seller initiated offer" | Wrong commit method used on a standard offer | `"Offer with id X is not buyer initiated"` |
91+
| "Quantity must be 1 for buyer initiated offers" | `createOffer` with `OfferCreator.Buyer` and `quantityAvailable: 2` | `"Quantity must be 1 for buyer initiated offers"` |
92+
93+
---
94+
95+
## Fund Flow Summary
96+
97+
```
98+
beforeEach: buyer deposits price → buyer account (available)
99+
seller deposits drFee → seller account (available)
100+
101+
commitToBuyerOffer():
102+
buyer available → 0 (moved to escrow)
103+
seller available → 0 (moved to escrow)
104+
```
105+
106+
---
107+
108+
## Key Assertions Checklist (successful commit)
109+
110+
- `exchange.state === ExchangeState.COMMITTED`
111+
- `exchange.offer.id` matches `buyerInitiatedOffer.id`
112+
- `exchange.seller.id` matches the committing seller's id
113+
- `exchange.seller.assistant` matches `sellerWallet.address` (lowercase)
114+
- `exchange.buyer.id` matches `buyerInitiatedOffer.buyerId`
115+
- `exchange.buyer.wallet` matches `buyerWallet.address` (lowercase)
116+
- Buyer funds `availableAmount === "0"` after commit
117+
- Seller funds `availableAmount === "0"` after commit
118+
- `offer.seller` is populated after commit (was falsy before)
119+
120+
---
121+
122+
## Files
123+
124+
- Test file: `e2e/tests/core-sdk-offers.test.ts`
125+
- Helpers: `e2e/tests/utils.ts`
126+
- `commitToBuyerOffer`
127+
- `commitToOffer`
128+
- `createOffer`
129+
- `createSeller`
130+
- `createDisputeResolver`
131+
- `initSellerAndBuyerSDKs`
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Synthesis: `createOfferAndCommit` in core-sdk.test.ts
2+
3+
## What the method does
4+
5+
`createOfferAndCommit` (helper in `e2e/tests/utils.ts`) atomically:
6+
1. Has the **offer creator** sign the offer (`signFullOffer`)
7+
2. Has the **committer** broadcast a single blockchain transaction that both *creates* the offer and *commits* to it
8+
3. Waits for the tx receipt, extracts `offerId` and `exchangeId` from logs
9+
4. Waits for the graph node to index the transaction
10+
5. Returns `{ offer, exchange }` fetched from the subgraph
11+
12+
Signature:
13+
```ts
14+
createOfferAndCommit(
15+
committerCoreSDK: CoreSDK, // the party who sends the tx and commits
16+
offerCreatorCoreSDK: CoreSDK, // the party who signs the offer
17+
fullOfferArgsUnsigned: Omit<FullOfferArgs, "signature">
18+
): Promise<{ offer, exchange }>
19+
```
20+
21+
The underlying SDK method `CoreSDK.createOfferAndCommit()` also accepts `{ returnTxInfo: true }` to return raw tx data instead of broadcasting.
22+
23+
---
24+
25+
## Two Main Use-Cases
26+
27+
### 1. Seller-Initiated Offers (`describe("seller-initiated offers")`)
28+
- **Offer creator** = seller (`sellerCoreSDK`)
29+
- **Committer** = buyer (`buyerCoreSDK`)
30+
- The seller pre-signs the offer; the buyer calls the combined tx
31+
- `quantityInitial` > 1 → multiple buyers can commit sequentially
32+
- After first commit: `quantityAvailable = quantityInitial - 1`
33+
34+
### 2. Buyer-Initiated Offers (`describe("buyer-initiated offers")`)
35+
- **Offer creator** = buyer (`buyerCoreSDK`)
36+
- **Committer** = seller (`sellerCoreSDK`)
37+
- The buyer pre-signs the offer; the seller calls the combined tx
38+
- `quantityAvailable` is always **1** → offer is sold out immediately after one commit
39+
- A second buyer attempting to commit gets: `"Offer with id X is sold out"`
40+
41+
---
42+
43+
## Prerequisites
44+
45+
### 1. Funded wallets for both parties
46+
`initSellerAndBuyerSDKs` (`utils.ts:263`) creates two fresh wallets (seller + buyer), each funded with ETH from a seed wallet. Both parties must have enough native ETH to pay for gas (and for the commit price in native-ETH offers).
47+
48+
### 2. Seller account registered on-chain — `createSeller` is mandatory
49+
`createSeller` (`utils.ts`) must be called before any `createOfferAndCommit`. It:
50+
- Stores seller metadata on IPFS
51+
- Calls `coreSDK.createSeller()` on-chain (registering `assistant`, `admin`, `treasury` addresses)
52+
- Returns the `sellerId` that is embedded in `fullOfferArgsUnsigned`
53+
54+
Without a registered seller, there is no `sellerId` to reference and the offer cannot be created.
55+
56+
### 3. Dispute resolver must exist and be active
57+
`checkDisputeResolver` (`core-sdk.test.ts:2215`) asserts that:
58+
- Dispute resolver #1 exists on-chain
59+
- `dr.active === true`
60+
- The seller is either on its allow-list or the allow-list is open (length = 0)
61+
62+
This is a protocol-level requirement: every offer must reference a valid, active dispute resolver that accepts the seller.
63+
64+
### 4. ERC20 tokens minted and approved (ERC20 offers only)
65+
For ERC20-token offers, `ensureMintedAndAllowedTokens` must be called for both wallets before committing.
66+
67+
### 5. `fullOfferArgsUnsigned` correctly built
68+
`buildFullOfferArgs` must receive the correct `committer`/`offerCreator` addresses, `sellerId`, `creator` enum (`OfferCreator.Seller` or `OfferCreator.Buyer`), and `quantityAvailable` (> 1 for seller-initiated, = 1 for buyer-initiated).
69+
70+
### Summary table
71+
72+
| Prerequisite | Seller-initiated | Buyer-initiated |
73+
|---|---|---|
74+
| Both wallets funded with ETH |||
75+
| `createSeller` called → `sellerId` obtained |||
76+
| Dispute resolver active & accepts seller |||
77+
| ERC20 minted + approved (ERC20 tests only) |||
78+
| `fullOfferArgsUnsigned` built with correct roles |||
79+
| `quantityAvailable` > 1 || ❌ (= 1) |
80+
81+
---
82+
83+
## Test Scenarios Covered
84+
85+
### Happy paths
86+
| Test | Committer | Creator | Token |
87+
|---|---|---|---|
88+
| Buyer commits to native offer | buyer | seller | ETH |
89+
| Buyer commits to ERC20 offer | buyer | seller | ERC20 |
90+
| Seller commits to native offer | seller | buyer | ETH |
91+
| Seller commits to ERC20 offer | seller | buyer | ERC20 |
92+
| Another buyer commits to same seller offer | buyer #2 | seller | ETH |
93+
94+
### Error / void paths
95+
| Test | Who voids | When | Error |
96+
|---|---|---|---|
97+
| `voidOffer` after first commit | seller (or buyer) | after commit | `offer.voided = true`, next commit fails |
98+
| `voidNonListedOffer` before first commit | seller (or buyer) | before commit | `"The offer has been voided"` |
99+
| `voidNonListedOfferBatch` (3 offers) | seller (or buyer) | before commit | `"The offer has been voided"` |
100+
| Buyer-initiated, second commit | n/a | after first commit | `"Offer with id X is sold out"` |
101+
102+
### Utility / inspection
103+
| Test | Purpose |
104+
|---|---|
105+
| `returnTxInfo: true` | Returns `{ data, to, value }` without broadcasting |
106+
107+
---
108+
109+
## Key Assertions (successful path)
110+
- `offer` is truthy and `offer.voided` is falsy
111+
- `offer.seller.id` matches the seller's id
112+
- `offer.quantityAvailable = quantityInitial - 1` (seller-initiated) or `0` (buyer-initiated)
113+
- `exchange.state === ExchangeState.COMMITTED`
114+
- `exchange.buyer.wallet` matches the buyer's address
115+
116+
---
117+
118+
## Files
119+
- Helper definition: `e2e/tests/utils.ts:660`
120+
- Test file: `e2e/tests/core-sdk.test.ts` (seller-initiated ~L245–430, buyer-initiated ~L534–680)

0 commit comments

Comments
 (0)