@bannynet/core-v6 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/USER_JOURNEYS.md CHANGED
@@ -1,525 +1,80 @@
1
- # banny-retail-v6 -- User Journeys
1
+ # User Journeys
2
2
 
3
- All interaction paths through `Banny721TokenUriResolver`, traced from entry point to final state. All function signatures, events, and revert names are verified against `src/Banny721TokenUriResolver.sol` and `src/interfaces/IBanny721TokenUriResolver.sol`.
3
+ ## Who This Repo Serves
4
4
 
5
- ---
5
+ - Banny collection operators publishing body, outfit, and background tiers
6
+ - collectors minting avatars and equipping accessories
7
+ - teams managing on-chain art payloads and metadata composition
6
8
 
7
- ## Journey 1: Buy a Naked Banny
9
+ ## Journey 1: Mint A Body, Outfit, And Background Set
8
10
 
9
- The resolver does not handle minting. Banny bodies are minted through the Juicebox 721 tier system. This journey is included for context because it produces the token that all other journeys depend on.
11
+ **Starting state:** the Banny collection is live through the 721 hook and the relevant tiers exist.
10
12
 
11
- **Entry point**: Payment to a `JBMultiTerminal` for the project that owns the `JB721TiersHook`. The hook's `afterPayRecordedWith` callback mints a 721 token from the appropriate tier.
13
+ **Success:** the collector owns the pieces needed to build a composed avatar.
12
14
 
13
- **Who can call**: Anyone (via `JBMultiTerminal.pay`).
15
+ **Flow**
16
+ 1. Mint the body, outfit, and background NFTs through the underlying Juicebox 721 project.
17
+ 2. Keep pricing, issuance, and treasury assumptions anchored in the 721 hook rather than this resolver.
18
+ 3. Treat this repo as the composition layer that activates once the user owns the right pieces.
14
19
 
15
- After minting, when any caller requests the token URI:
20
+ ## Journey 2: Dress A Banny And Put Accessories Into Resolver Custody
16
21
 
17
- ```
18
- JB721TiersHook.tokenURI(tokenId)
19
- -> Banny721TokenUriResolver.tokenUriOf(hook, tokenId)
20
- ```
22
+ **Starting state:** the collector owns a body plus compatible accessories.
21
23
 
22
- **Function**: `tokenUriOf(address hook, uint256 tokenId) external view returns (string memory)`
24
+ **Success:** the chosen outfit and background are attached to the body and the resolver renders the combined look.
23
25
 
24
- **Parameters**:
25
- - `hook` -- The `JB721TiersHook` contract address
26
- - `tokenId` -- The minted token ID (e.g., `4_000_000_001` for Original Banny #1)
26
+ **Flow**
27
+ 1. Call `decorateBannyWith(...)` for the target body.
28
+ 2. `Banny721TokenUriResolver` checks compatibility rules and takes custody of the attached accessory NFTs while they are equipped.
29
+ 3. The body's token URI now resolves to a layered SVG and metadata payload reflecting the active composition.
27
30
 
28
- **State read**:
29
- 1. `_productOfTokenId(hook, tokenId)` calls `hook.STORE().tierOfTokenId(hook, tokenId, false)`
30
- 2. If `product.category == 0` (body), calls `svgOf(hook, tokenId, true, true)`
31
- 3. `svgOf` calls `assetIdsOf(hook, tokenId)` which returns empty arrays (no outfits or background attached)
32
- 4. The body SVG is composed with default accessories (necklace, eyes, mouth) and no custom outfits
31
+ ## Journey 3: Lock A Banny's Appearance For A Period
33
32
 
34
- **Result**: A base64-encoded data URI containing JSON metadata with:
35
- - Product name (Alien, Pink, Orange, or Original)
36
- - Category name ("Banny body")
37
- - An SVG image with the naked banny body and default accessories
38
- - Empty `outfitIds` array, `backgroundId: 0`
33
+ **Starting state:** the collector likes the current look and does not want it changed immediately.
39
34
 
40
- **Events**: None (view function).
35
+ **Success:** the avatar's appearance is frozen for the lock window and later equipment changes must wait.
41
36
 
42
- **Edge cases**:
43
- - **Token ID does not exist**: `_productOfTokenId` returns a tier with `id == 0`. Function returns `""`
44
- - **SVG content not uploaded**: Falls back to IPFS URI via `JBIpfsDecoder`
45
- - **Token from category > 17**: Falls back to hook's `baseURI()` for IPFS resolution
46
- - **Unrecognized product**: Reverts `Banny721TokenUriResolver_UnrecognizedProduct`
37
+ **Flow**
38
+ 1. Call `lockOutfitChangesFor(...)` on the resolver.
39
+ 2. The resolver records the lock period for that body.
40
+ 3. Future decorate or removal actions respect the lock until it expires.
47
41
 
48
- ---
42
+ ## Journey 4: Publish Or Repair On-Chain Art Assets
49
43
 
50
- ## Journey 2: Dress a Banny with Outfits
44
+ **Starting state:** the collection's visual payloads are referenced by content hashes but the actual SVG payloads still need to be made available.
51
45
 
52
- **Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 bannyBodyId, uint256 backgroundId, uint256[] calldata outfitIds)`
46
+ **Success:** token URIs render complete art instead of placeholders or missing layers.
53
47
 
54
- **Who can call**: The owner of the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`). Protected by `ReentrancyGuard`. ERC-2771 meta-transactions supported.
48
+ **Flow**
49
+ 1. Register the content hashes for bodies, outfits, or backgrounds with `setSvgHashesOf(...)`.
50
+ 2. Upload or repair the corresponding SVG payloads with `setSvgContentsOf(...)`.
51
+ 3. Re-resolve token URIs to confirm the on-chain composition now renders correctly.
55
52
 
56
- **Prerequisites**:
57
- - Caller owns the banny body NFT
58
- - Body is not locked (`outfitLockedUntil[hook][bannyBodyId] <= block.timestamp`)
59
- - Caller owns each outfit NFT directly, or owns the banny body currently wearing that outfit
60
- - Caller owns the background NFT directly (or owns the banny body currently using it), if `backgroundId != 0`
61
- - Caller has approved the resolver for the hook contract (`hook.setApprovalForAll(resolver, true)`)
53
+ **Failure cases that matter:** publishing content that does not match the registered hash, forgetting to set product names for new pieces, and assuming the 721 hook owns the art payload when this repo owns the rendered output.
62
54
 
63
- **Parameters**:
64
- - `hook` -- The `JB721TiersHook` contract address
65
- - `bannyBodyId` -- Token ID of the banny body being dressed. Must be category 0
66
- - `backgroundId` -- Token ID of the background to attach. Pass `0` for no background
67
- - `outfitIds` -- Array of outfit token IDs. Must be in ascending category order (categories 2--17). Pass `[]` for no outfits
55
+ ## Journey 5: Update Collection Metadata And Product Catalog Entries
68
56
 
69
- **State changes**:
70
- 1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- verifies caller owns the body
71
- 2. `_productOfTokenId(hook, bannyBodyId).category` must equal `_BODY_CATEGORY` (0)
72
- 3. `outfitLockedUntil[hook][bannyBodyId]` must be `<= block.timestamp`
73
- 4. Background processing (`_decorateBannyWithBackground`) -- only executes if the background is changing (new `backgroundId` differs from the current one, or the current background is no longer assigned to this body):
74
- - If a previous background was assigned to this body: old background NFT return attempted via `_tryTransferFrom`. If the return fails, the background change is **aborted** — old background stays attached, new background is not equipped
75
- - On successful return: `_attachedBackgroundIdOf[hook][bannyBodyId]` updated to `backgroundId` (or cleared to 0)
76
- - If `backgroundId != 0`: `_userOf[hook][backgroundId]` set to `bannyBodyId`
77
- - If `backgroundId != 0` and the resolver does not already hold the new background: new background NFT transferred into the resolver via `_transferFrom` (reverts on failure)
78
- - If `backgroundId == 0` and the return transfer fails: `_attachedBackgroundIdOf` is **not cleared** (background stays attached for recovery)
79
- 5. Outfit processing (`_decorateBannyWithOutfits`):
80
- - For each new outfit not already worn by this body: `_wearerOf[hook][outfitId]` set to `bannyBodyId`
81
- - If there are previous outfits in categories up to the current one: old outfits transfer attempted via `_tryTransferFrom`. If a transfer fails, the outfit is **retained** (not zeroed in the previous outfit tracking array)
82
- - For each new outfit not already held by the resolver: transferred into the resolver via `_transferFrom`
83
- - `_storeOutfitsWithRetained` merges the new `outfitIds` with any retained outfits whose transfers failed, then stores the combined array in `_attachedOutfitIdsOf[hook][bannyBodyId]`
57
+ **Starting state:** the collection exists, but its descriptive metadata or UPC-to-name catalog needs to change.
84
58
 
85
- **Events**: `DecorateBanny(address indexed hook, uint256 indexed bannyBodyId, uint256 indexed backgroundId, uint256[] outfitIds, address caller)` -- emitted before state changes, where `caller = _msgSender()`
59
+ **Success:** token URIs and collection-level presentation reflect the intended description, external URL, base URI, and product naming.
86
60
 
87
- **Edge cases**:
88
- - **Empty outfitIds with backgroundId=0**: Strips all outfits and background. All previously equipped NFTs returned to caller
89
- - **Outfit already worn by this body**: Not re-transferred. `_wearerOf` retains existing value. The outfit stays in resolver custody
90
- - **Outfit worn by another body owned by caller**: Authorized via `ownerOf(wearerId) == _msgSender()`. The outfit is unlinked from the old body and transferred to the new body's custody
91
- - **Burned outfit in previous set**: `_tryTransferFrom` returns `false`. The burned outfit is **retained** in the attachment array (phantom entry) rather than removed. This prevents stranding of other assets and keeps the record for potential future recovery
92
- - **Removed tier in previous set**: `_productOfTokenId` returns category 0. The while loop processes it (category 0 <= any new category) and transfers it out
93
- - **Categories not ascending**: Reverts `Banny721TokenUriResolver_UnorderedCategories`
94
- - **Duplicate categories**: Reverts `Banny721TokenUriResolver_UnorderedCategories` (equality fails the `<` check)
95
- - **Category outside 2--17 as outfit**: Reverts `Banny721TokenUriResolver_UnrecognizedCategory`
96
- - **Head category conflict**: Reverts `Banny721TokenUriResolver_HeadAlreadyAdded`
97
- - **Suit category conflict**: Reverts `Banny721TokenUriResolver_SuitAlreadyAdded`
98
- - **Unauthorized background**: Reverts `Banny721TokenUriResolver_UnauthorizedBackground`
99
- - **Unauthorized outfit**: Reverts `Banny721TokenUriResolver_UnauthorizedOutfit`
100
- - **Unrecognized background category**: Reverts `Banny721TokenUriResolver_UnrecognizedBackground`
101
- - **Source body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked` (via `_revertIfBodyLocked`)
102
- - **Reentrancy via safeTransferFrom callback**: Blocked by `nonReentrant` modifier
61
+ **Flow**
62
+ 1. Update collection metadata with `setMetadata(...)`.
63
+ 2. Set or repair product names for the UPCs the renderer should expose with `setProductNames(...)`.
64
+ 3. Re-check token URI output so the rendered Banny and its catalog labels agree.
103
65
 
104
- ### Example: Dress with Hat and Glasses
66
+ ## Journey 6: Unequip And Recover Custodied Accessories
105
67
 
106
- ```
107
- Precondition: Alice owns body #4_000_000_001 (Original, category 0)
108
- Alice owns hat #5_000_000_001 (category 4, Head)
109
- Alice owns glasses #6_000_000_001 (category 6, Glasses)
110
- Alice has called hook.setApprovalForAll(resolver, true)
68
+ **Starting state:** a body has attached pieces held by the resolver and the owner wants to rearrange or transfer them.
111
69
 
112
- Call: decorateBannyWith(hook, 4_000_000_001, 0, [5_000_000_001, 6_000_000_001])
70
+ **Success:** the accessories leave resolver custody and can be reused or transferred independently.
113
71
 
114
- REVERTS -- Head (category 4) blocks Glasses (category 6) per conflict rule.
115
- ```
72
+ **Flow**
73
+ 1. Remove or replace the equipped items once no lock blocks the change.
74
+ 2. The resolver releases custody of the old accessory NFTs.
75
+ 3. The owner can now transfer, burn, or re-equip those pieces elsewhere.
116
76
 
117
- ```
118
- Corrected: decorateBannyWith(hook, 4_000_000_001, 0, [5_000_000_001])
77
+ ## Hand-Offs
119
78
 
120
- Result:
121
- - Hat NFT transferred from Alice to resolver.
122
- - _wearerOf[hook][5_000_000_001] = 4_000_000_001
123
- - _attachedOutfitIdsOf[hook][4_000_000_001] = [5_000_000_001]
124
- - Body's tokenURI now renders with hat layer.
125
- ```
126
-
127
- ---
128
-
129
- ## Journey 3: Undress a Banny
130
-
131
- Undressing is not a separate function. It is performed by calling `decorateBannyWith` with empty arrays.
132
-
133
- **Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 bannyBodyId, 0, [])`
134
-
135
- **Who can call**: The owner of the banny body NFT.
136
-
137
- **Parameters**:
138
- - `hook` -- The hook address
139
- - `bannyBodyId` -- The banny body to undress
140
- - `backgroundId` -- `0` (remove background)
141
- - `outfitIds` -- `[]` (remove all outfits)
142
-
143
- **State changes**:
144
- 1. All checks pass (ownership, not locked, body category)
145
- 2. If a background was attached: old background NFT return attempted via `_tryTransferFrom`. If successful, `_attachedBackgroundIdOf[hook][bannyBodyId]` set to 0. If the transfer fails (e.g., owner is a contract that rejects ERC-721), the background **stays attached** for recovery
146
- 3. The new `outfitIds` array is empty, so the merge loop does not execute. If there are previous outfits: the tail loop attempts to transfer out all previous outfits via `_tryTransferFrom`. Failed transfers are **retained** in the attached list
147
- 4. `_storeOutfitsWithRetained` stores `[]` plus any retained outfits in `_attachedOutfitIdsOf[hook][bannyBodyId]`
148
- 5. Successfully transferred outfits and backgrounds are returned to `_msgSender()`. Failed transfers remain in resolver custody but stay tracked
149
-
150
- **Events**: `DecorateBanny(hook, bannyBodyId, 0, [], caller)`
151
-
152
- **Partial undress**: To remove some outfits but keep others, pass only the outfits you want to keep:
153
-
154
- ```
155
- Before: outfitIds = [hat, glasses, necklace]
156
- Call: decorateBannyWith(hook, bodyId, backgroundId, [necklace])
157
- After: hat and glasses returned to caller, necklace stays equipped.
158
- ```
159
-
160
- **Edge cases**:
161
- - **Body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked`. Cannot undress during lock period
162
- - **Some equipped outfits were burned**: `_tryTransferFrom` returns `false` for burned tokens. No revert — the burned outfits are **retained** in the attached list as phantom entries. No NFT is returned for those tokens
163
- - **Caller is not the current owner**: Reverts `Banny721TokenUriResolver_UnauthorizedBannyBody`. This can happen if the body was recently transferred
164
-
165
- ---
166
-
167
- ## Journey 4: Lock a Banny
168
-
169
- **Entry point**: `Banny721TokenUriResolver.lockOutfitChangesFor(address hook, uint256 bannyBodyId)`
170
-
171
- **Who can call**: The owner of the banny body NFT (`IERC721(hook).ownerOf(bannyBodyId) == _msgSender()`).
172
-
173
- **Parameters**:
174
- - `hook` -- The hook address
175
- - `bannyBodyId` -- The banny body to lock
176
-
177
- **State changes**:
178
- 1. `_checkIfSenderIsOwner(hook, bannyBodyId)` -- verifies caller owns the body
179
- 2. `newLockUntil = block.timestamp + 7 days` (constant `_LOCK_DURATION`)
180
- 3. If `currentLockedUntil > newLockUntil`, reverts
181
- 4. `outfitLockedUntil[hook][bannyBodyId] = newLockUntil`
182
-
183
- **Events**: None. The `lockOutfitChangesFor` function does not emit an event.
184
-
185
- **Edge cases**:
186
- - **Relocking while already locked**: Succeeds if the new expiry is >= the current expiry. Since `newLockUntil = block.timestamp + 7 days`, this effectively extends the lock by whatever time remains
187
- - **Attempting to shorten the lock**: Reverts `Banny721TokenUriResolver_CantAccelerateTheLock`
188
- - **Locking an undressed body**: Valid. The body is locked with no outfits. No outfits can be added during the lock period
189
- - **Lock after transfer**: The new owner can lock. The old owner cannot (they no longer pass `_checkIfSenderIsOwner`)
190
- - **Lock does not prevent body transfer**: The lock only affects `decorateBannyWith`. The body NFT can still be transferred on the hook contract
191
- - **No admin override**: The contract owner cannot unlock a body. The lock must expire naturally
192
-
193
- ### Use Case: Marketplace Sale
194
-
195
- ```
196
- 1. Alice owns body #1 dressed with rare hat and suit.
197
- 2. Alice calls lockOutfitChangesFor(hook, bodyId).
198
- 3. Alice lists body #1 on marketplace.
199
- 4. Bob buys body #1. Body transfers to Bob.
200
- 5. Lock persists -- Bob receives the body with guaranteed outfits.
201
- 6. After 7 days, Bob can undress and receive the hat and suit NFTs.
202
- ```
203
-
204
- ---
205
-
206
- ## Journey 5: Transfer a Decorated Banny
207
-
208
- The resolver does not intercept or control body transfers. Body transfers happen on the `JB721TiersHook` contract. The resolver observes the ownership change lazily.
209
-
210
- **Entry point**: `JB721TiersHook.safeTransferFrom(address from, address to, uint256 tokenId)` (standard ERC-721 transfer on the hook contract, not the resolver)
211
-
212
- **Who can call**: The body owner, or an approved operator on the hook contract.
213
-
214
- **State changes**:
215
- 1. No resolver functions are called during the transfer
216
- 2. Resolver state remains unchanged:
217
- - `_attachedOutfitIdsOf[hook][bodyId]` still contains the outfit array
218
- - `_attachedBackgroundIdOf[hook][bodyId]` still contains the background ID
219
- - `_wearerOf[hook][outfitId]` still maps each outfit to `bodyId`
220
- - `_userOf[hook][backgroundId]` still maps to `bodyId`
221
- 3. All equipped outfit and background NFTs remain held by the resolver
222
-
223
- **Events**: None from the resolver. The hook emits the standard ERC-721 `Transfer(from, to, tokenId)`.
224
-
225
- **What the new owner (Bob) can do**:
226
- - **Undress**: `decorateBannyWith(hook, bodyId, 0, [])` returns all equipped NFTs to Bob
227
- - **Redress**: `decorateBannyWith(hook, bodyId, newBg, [newOutfits])` replaces outfits. Old outfits returned to Bob (even though Alice originally equipped them)
228
- - **Lock**: `lockOutfitChangesFor(hook, bodyId)` locks the body under Bob's control
229
-
230
- **What Alice can no longer do**: Alice cannot call `decorateBannyWith` or `lockOutfitChangesFor` for that body -- she no longer owns it.
231
-
232
- **Edge cases**:
233
- - **Alice equipped valuable outfits and forgot to undress before selling**: Bob receives full control of all equipped NFTs. This is the intended behavior but creates a seller gotcha. The lock mechanism exists to make this explicit (sellers lock, then sell at a higher price including outfits)
234
- - **Body transferred while locked**: Lock persists. Bob cannot change outfits until the lock expires
235
- - **Body transferred to a contract**: The new contract owner must be able to call `decorateBannyWith`. If the contract does not implement this call, equipped outfits are effectively locked forever (until the contract is upgraded or has a function to make this call)
236
- - **Double transfer (Alice -> Bob -> Charlie)**: Only Charlie can interact with the body's outfits. Each transfer implicitly transfers outfit control
237
-
238
- ---
239
-
240
- ## Journey 6: Move an Outfit Between Bodies
241
-
242
- A user who owns multiple bodies can move an equipped outfit from one body to another in a single call.
243
-
244
- **Entry point**: `Banny721TokenUriResolver.decorateBannyWith(address hook, uint256 newBodyId, 0, [outfitId])`
245
-
246
- Where `outfitId` is currently equipped on `oldBodyId`, and the caller owns both bodies.
247
-
248
- **Who can call**: An address that owns both `newBodyId` and the body currently wearing the outfit (`oldBodyId`).
249
-
250
- **Prerequisites**:
251
- - Caller owns `newBodyId`
252
- - Caller owns `oldBodyId` (which currently wears the outfit)
253
- - `newBodyId` is not locked
254
- - `oldBodyId` is not locked (enforced by `_revertIfBodyLocked`)
255
-
256
- **State changes**:
257
- 1. Authorization passes: caller does not own the outfit directly (resolver holds it), but caller owns `oldBodyId` which is the `wearerOf(hook, outfitId)`
258
- 2. `_wearerOf[hook][outfitId]` updated to `newBodyId`
259
- 3. The outfit is not transferred (resolver already holds it)
260
- 4. `_attachedOutfitIdsOf[hook][newBodyId]` set to the new array including this outfit
261
- 5. `_attachedOutfitIdsOf[hook][oldBodyId]` is NOT explicitly updated. However, `wearerOf(hook, outfitId)` now returns `newBodyId`, so `assetIdsOf(hook, oldBodyId)` will exclude this outfit from its filtered result
262
-
263
- **Events**: `DecorateBanny(hook, newBodyId, 0, [outfitId], caller)`
264
-
265
- **Edge cases**:
266
- - **Old body is locked**: Reverts `Banny721TokenUriResolver_OutfitChangesLocked` via `_revertIfBodyLocked`. A locked source body keeps its outfits and background until the lock expires, even if the caller owns both bodies
267
-
268
- ---
269
-
270
- ## Journey 7: Admin -- Store SVG Content
271
-
272
- ### Step 1: Commit Hashes
273
-
274
- **Entry point**: `Banny721TokenUriResolver.setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes)`
275
-
276
- **Who can call**: Contract owner only (`onlyOwner` modifier, from OpenZeppelin `Ownable`).
277
-
278
- **Parameters**:
279
- - `upcs` -- Array of universal product codes to set hashes for
280
- - `svgHashes` -- Array of `keccak256` hashes of the SVG content strings
281
-
282
- **State changes**:
283
- 1. For each pair: `svgHashOf[upc] = svgHash`
284
-
285
- **Events**: `SetSvgHash(uint256 indexed upc, bytes32 indexed svgHash, address caller)` -- emitted once per UPC in the array
286
-
287
- **Edge cases**:
288
- - **UPC already has a hash**: Reverts `Banny721TokenUriResolver_HashAlreadyStored` (write-once)
289
- - **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
290
- - **Partial failure**: The entire call reverts if any single UPC fails
291
-
292
- ### Step 2: Upload Content
293
-
294
- **Entry point**: `Banny721TokenUriResolver.setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents)`
295
-
296
- **Who can call**: Anyone. Not restricted to owner -- anyone can upload content as long as it matches the committed hash.
297
-
298
- **Parameters**:
299
- - `upcs` -- Array of universal product codes to upload content for
300
- - `svgContents` -- Array of SVG content strings (without wrapping `<svg>` tags)
301
-
302
- **State changes**:
303
- 1. For each pair: `_svgContentOf[upc] = svgContent`
304
-
305
- **Events**: `SetSvgContent(uint256 indexed upc, string svgContent, address caller)` -- emitted once per UPC in the array
306
-
307
- **Edge cases**:
308
- - **Content already stored**: Reverts `Banny721TokenUriResolver_ContentsAlreadyStored` (write-once)
309
- - **No hash set for UPC**: Reverts `Banny721TokenUriResolver_HashNotFound`
310
- - **Content does not match hash**: Reverts `Banny721TokenUriResolver_ContentsMismatch` (checks `keccak256(abi.encodePacked(svgContent)) != svgHashOf[upc]`)
311
- - **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
312
- - **Hash set but content never uploaded**: `_svgOf` falls back to IPFS resolution. Tokens render with IPFS images instead of on-chain SVG
313
- - **Content with special characters**: Stored verbatim. No sanitization. A `<script>` tag in SVG content would be included in the data URI
314
-
315
- ---
316
-
317
- ## Journey 8: Admin -- Set Metadata and Product Names
318
-
319
- ### Set Metadata
320
-
321
- **Entry point**: `Banny721TokenUriResolver.setMetadata(string calldata description, string calldata url, string calldata baseUri)`
322
-
323
- **Who can call**: Contract owner only (`onlyOwner` modifier).
324
-
325
- **Parameters**:
326
- - `description` -- The description to use in token metadata
327
- - `url` -- The external URL to use in token metadata
328
- - `baseUri` -- The base URI of the SVG files (used for IPFS fallback)
329
-
330
- **State changes**:
331
- 1. `svgDescription = description`
332
- 2. `svgExternalUrl = url`
333
- 3. `svgBaseUri = baseUri`
334
- 4. All three fields are always overwritten. Pass current values for fields you do not want to change. Pass `""` to clear
335
-
336
- **Events**: `SetMetadata(string description, string externalUrl, string baseUri, address caller)`
337
-
338
- **Edge cases**:
339
- - **Empty strings**: Valid. Clears the respective metadata field
340
- - **Non-owner caller**: Reverts with OpenZeppelin `OwnableUnauthorizedAccount`
341
-
342
- ### Set Product Names
343
-
344
- **Entry point**: `Banny721TokenUriResolver.setProductNames(uint256[] memory upcs, string[] memory names)`
345
-
346
- **Who can call**: Contract owner only (`onlyOwner` modifier).
347
-
348
- **Parameters**:
349
- - `upcs` -- Array of universal product codes to name
350
- - `names` -- Array of display names for each product
351
-
352
- **State changes**:
353
- 1. For each pair: `_customProductNameOf[upc] = name`
354
-
355
- **Events**: `SetProductName(uint256 indexed upc, string name, address caller)` -- emitted once per UPC in the array
356
-
357
- **Edge cases**:
358
- - **Overwriting a product name**: No revert. The old name is replaced. This could change how existing NFTs display (names are mutable, unlike SVG hashes/content)
359
- - **Setting name for UPCs 1--4**: The `_customProductNameOf` mapping is written, but `_productNameOf` returns the hardcoded name first (Alien, Pink, Orange, Original). The custom name is never read for these UPCs
360
- - **Empty name string**: Valid. Sets the custom name to empty, causing `_productNameOf` to return `""` for that UPC
361
- - **Array length mismatch**: Reverts `Banny721TokenUriResolver_ArrayLengthMismatch`
362
-
363
- ---
364
-
365
- ## Journey 9: View Functions -- Query Banny State
366
-
367
- All view functions. No access restrictions, no state changes, no events.
368
-
369
- ### Get Attached Assets
370
-
371
- **Entry point**: `Banny721TokenUriResolver.assetIdsOf(address hook, uint256 bannyBodyId) public view returns (uint256 backgroundId, uint256[] memory outfitIds)`
372
-
373
- **Who can call**: Anyone (view function).
374
-
375
- **Parameters**:
376
- - `hook` -- The hook address of the collection
377
- - `bannyBodyId` -- The banny body to query
378
-
379
- Returns the currently attached background and outfits. Filters by checking `wearerOf` and `userOf` for each stored ID, excluding outfits that have been moved to other bodies.
380
-
381
- ### Get Outfit Wearer
382
-
383
- **Entry point**: `Banny721TokenUriResolver.wearerOf(address hook, uint256 outfitId) public view returns (uint256)`
384
-
385
- **Who can call**: Anyone (view function).
386
-
387
- **Parameters**:
388
- - `hook` -- The hook address of the collection
389
- - `outfitId` -- The outfit token ID to query
390
-
391
- Returns the body ID wearing this outfit, or 0 if unworn. Verifies the outfit is still in the body's `_attachedOutfitIdsOf` array.
392
-
393
- ### Get Background User
394
-
395
- **Entry point**: `Banny721TokenUriResolver.userOf(address hook, uint256 backgroundId) public view returns (uint256)`
396
-
397
- **Who can call**: Anyone (view function).
398
-
399
- **Parameters**:
400
- - `hook` -- The hook address of the collection
401
- - `backgroundId` -- The background token ID to query
402
-
403
- Returns the body ID using this background, or 0 if unused. Verifies the background is still the body's `_attachedBackgroundIdOf` entry.
404
-
405
- ### Get SVG
406
-
407
- **Entry point**: `Banny721TokenUriResolver.svgOf(address hook, uint256 tokenId, bool shouldDressBannyBody, bool shouldIncludeBackgroundOnBannyBody) public view returns (string memory)`
408
-
409
- **Who can call**: Anyone (view function).
410
-
411
- **Parameters**:
412
- - `hook` -- The hook address of the collection
413
- - `tokenId` -- The token ID to render
414
- - `shouldDressBannyBody` -- Whether to include the banny body's attached outfits
415
- - `shouldIncludeBackgroundOnBannyBody` -- Whether to include the banny body's attached background
416
-
417
- Returns the composed SVG for any token. For bodies, can toggle dressing and background. For non-bodies, returns the outfit/background SVG alone.
418
-
419
- ### Get Names
420
-
421
- **Entry point**: `Banny721TokenUriResolver.namesOf(address hook, uint256 tokenId) public view returns (string memory, string memory, string memory)`
422
-
423
- **Who can call**: Anyone (view function).
424
-
425
- **Parameters**:
426
- - `hook` -- The hook address of the collection
427
- - `tokenId` -- The token ID to look up
428
-
429
- Returns `(fullName, categoryName, productName)`.
430
-
431
- ### Get Lock Status
432
-
433
- **Entry point**: `Banny721TokenUriResolver.outfitLockedUntil(address hook, uint256 upc) public view returns (uint256)`
434
-
435
- **Who can call**: Anyone (public mapping).
436
-
437
- **Parameters**:
438
- - `hook` -- The hook address of the collection
439
- - `upc` -- The banny body token ID
440
-
441
- Returns the timestamp until which the body is locked, or 0 if never locked.
442
-
443
- ---
444
-
445
- ## Summary: State Machine per Banny Body
446
-
447
- ```
448
- +-----------+
449
- | NAKED |
450
- | (minted) |
451
- +-----+-----+
452
- |
453
- decorateBannyWith(outfits)
454
- |
455
- +-----v-----+
456
- | DRESSED |<----+
457
- | | |
458
- +--+--+--+--+ |
459
- | | | |
460
- +------------+ | +--------+
461
- | | decorateBannyWith(newOutfits)
462
- decorateBannyWith([]) |
463
- | | lockOutfitChangesFor()
464
- +-----v-----+ |
465
- | NAKED | |
466
- +-----+-----+ |
467
- | |
468
- lockOutfitChangesFor() |
469
- | |
470
- +-----v---------------v--+
471
- | LOCKED |
472
- | (7 days) |
473
- +-----+-------------------+
474
- |
475
- block.timestamp > lockUntil
476
- |
477
- +-----v-----------+
478
- | UNLOCKED |
479
- | (NAKED or |
480
- | DRESSED) |
481
- +-----------------+
482
- ```
483
-
484
- The lock state applies equally to dressed and naked bodies. A dressed body can be locked, preventing outfit changes. A naked body can be locked, preventing outfit additions.
485
-
486
- Body transfers do not change the resolver state. The new owner inherits the current state (dressed/naked, locked/unlocked) and all custody rights.
487
-
488
- ---
489
-
490
- ## Events Reference
491
-
492
- All events defined in `IBanny721TokenUriResolver`:
493
-
494
- | Event | Emitted by | Signature |
495
- |-------|-----------|-----------|
496
- | `DecorateBanny` | `decorateBannyWith` | `DecorateBanny(address indexed hook, uint256 indexed bannyBodyId, uint256 indexed backgroundId, uint256[] outfitIds, address caller)` |
497
- | `SetMetadata` | `setMetadata` | `SetMetadata(string description, string externalUrl, string baseUri, address caller)` |
498
- | `SetProductName` | `setProductNames` | `SetProductName(uint256 indexed upc, string name, address caller)` |
499
- | `SetSvgContent` | `setSvgContentsOf` | `SetSvgContent(uint256 indexed upc, string svgContent, address caller)` |
500
- | `SetSvgHash` | `setSvgHashesOf` | `SetSvgHash(uint256 indexed upc, bytes32 indexed svgHash, address caller)` |
501
-
502
- ## Custom Errors Reference
503
-
504
- All custom errors defined in `Banny721TokenUriResolver`:
505
-
506
- | Error | Trigger |
507
- |-------|---------|
508
- | `Banny721TokenUriResolver_ArrayLengthMismatch` | `upcs` and values arrays have different lengths |
509
- | `Banny721TokenUriResolver_BannyBodyNotBodyCategory` | `bannyBodyId` is not a category 0 (body) token |
510
- | `Banny721TokenUriResolver_CantAccelerateTheLock` | New lock expiry would be earlier than current lock |
511
- | `Banny721TokenUriResolver_ContentsAlreadyStored` | SVG content already uploaded for this UPC |
512
- | `Banny721TokenUriResolver_ContentsMismatch` | SVG content hash does not match committed hash |
513
- | `Banny721TokenUriResolver_HashAlreadyStored` | SVG hash already committed for this UPC |
514
- | `Banny721TokenUriResolver_HashNotFound` | No hash committed for this UPC |
515
- | `Banny721TokenUriResolver_HeadAlreadyAdded` | Outfit conflicts with an already-equipped head item |
516
- | `Banny721TokenUriResolver_OutfitChangesLocked` | Body is locked and cannot change outfits |
517
- | `Banny721TokenUriResolver_SuitAlreadyAdded` | Outfit conflicts with an already-equipped suit item |
518
- | `Banny721TokenUriResolver_UnauthorizedBackground` | Caller does not own the background or the body using it |
519
- | `Banny721TokenUriResolver_UnauthorizedBannyBody` | Caller does not own the banny body |
520
- | `Banny721TokenUriResolver_UnauthorizedOutfit` | Caller does not own the outfit or the body wearing it |
521
- | `Banny721TokenUriResolver_UnauthorizedTransfer` | NFT received from an external sender (not this contract) |
522
- | `Banny721TokenUriResolver_UnorderedCategories` | Outfit categories are not in ascending order |
523
- | `Banny721TokenUriResolver_UnrecognizedBackground` | Token is not a valid background category |
524
- | `Banny721TokenUriResolver_UnrecognizedCategory` | Outfit category is not in the valid range (2--17) |
525
- | `Banny721TokenUriResolver_UnrecognizedProduct` | Token does not belong to a recognized product tier |
79
+ - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md) for mint pricing, tier issuance, reserves, and treasury behavior.
80
+ - Use this repo only once the question is about custody, compatibility, outfit locks, or SVG composition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bannynet/core-v6",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,12 +20,12 @@
20
20
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'banny-core-v6'"
21
21
  },
22
22
  "dependencies": {
23
- "@bananapus/721-hook-v6": "^0.0.26",
23
+ "@bananapus/721-hook-v6": "^0.0.30",
24
24
  "@bananapus/core-v6": "^0.0.28",
25
25
  "@bananapus/permission-ids-v6": "^0.0.14",
26
26
  "@bananapus/router-terminal-v6": "^0.0.21",
27
27
  "@bananapus/suckers-v6": "^0.0.18",
28
- "@croptop/core-v6": "^0.0.26",
28
+ "@croptop/core-v6": "^0.0.28",
29
29
  "@openzeppelin/contracts": "^5.6.1",
30
30
  "@rev-net/core-v6": "^0.0.24",
31
31
  "keccak": "^3.0.4"
@@ -0,0 +1,25 @@
1
+ # Banny Operations
2
+
3
+ ## Content and Deployment Surface
4
+
5
+ - [`script/Deploy.s.sol`](../script/Deploy.s.sol) is the main deployment entry point.
6
+ - [`script/Drop1.s.sol`](../script/Drop1.s.sol) and [`script/Add.Denver.s.sol`](../script/Add.Denver.s.sol) are the first places to check when a problem is drop-specific rather than core resolver logic.
7
+ - The resolver's metadata and SVG-content management functions live in [`src/Banny721TokenUriResolver.sol`](../src/Banny721TokenUriResolver.sol), not in separate admin helpers.
8
+
9
+ ## Change Checklist
10
+
11
+ - If you edit decoration behavior, verify both attachment and return-to-owner paths.
12
+ - If you edit SVG rendering, re-check default layer injection and non-body preview rendering.
13
+ - If you edit content upload assumptions, verify hash registration and one-time content storage still match.
14
+ - If you edit metadata fields, check whether the issue belongs in resolver config or in the upstream hook that points to the resolver.
15
+
16
+ ## Common Failure Modes
17
+
18
+ - Visible rendering issue is really stale or missing SVG content rather than code logic.
19
+ - Resolver is blamed for minting or tier problems that actually live upstream in the 721 hook repo.
20
+ - Attachment state looks inconsistent because a prior transfer or return failed and the resolver intentionally preserved safety over convenience.
21
+
22
+ ## Useful Proof Points
23
+
24
+ - [`test/audit/`](../test/audit/) for security-sensitive assumptions.
25
+ - [`script/helpers/`](../script/helpers/) when a deployment issue is really a script/config problem.
@@ -0,0 +1,27 @@
1
+ # Banny Runtime
2
+
3
+ ## Contract Role
4
+
5
+ - [`src/Banny721TokenUriResolver.sol`](../src/Banny721TokenUriResolver.sol) resolves token metadata, stores equipped outfits and backgrounds, enforces outfit locks, and composes layered SVG output for Banny collections.
6
+
7
+ ## Runtime Path
8
+
9
+ 1. The hook calls the resolver for `tokenURI`-style metadata.
10
+ 2. The resolver reads tier and ownership context from the upstream 721 hook.
11
+ 3. If the token is a body, it composes background, body, and equipped items into a single SVG.
12
+ 4. If the token is an outfit or background, it renders a preview-style representation instead.
13
+ 5. During decoration flows, the resolver takes custody of attached items and updates wearer/background mappings.
14
+
15
+ ## High-Risk Areas
16
+
17
+ - Attachment custody: equipped items are held by the resolver, so transfer and return behavior matters.
18
+ - Outfit lock windows: lock duration is part of user-facing state and should not drift unexpectedly.
19
+ - Rendering composition: layer ordering and default-item behavior affect visible output and must stay deterministic.
20
+ - Stale attachment cleanup: views intentionally guard against inconsistent attachment state.
21
+
22
+ ## Tests To Trust First
23
+
24
+ - [`test/DecorateFlow.t.sol`](../test/DecorateFlow.t.sol) for the main equip/unequip lifecycle.
25
+ - [`test/OutfitTransferLifecycle.t.sol`](../test/OutfitTransferLifecycle.t.sol) for custody and return behavior.
26
+ - [`test/BannyAttacks.t.sol`](../test/BannyAttacks.t.sol) for adversarial flows.
27
+ - [`test/TestQALastMile.t.sol`](../test/TestQALastMile.t.sol) and [`test/regression/`](../test/regression/) for pinned edge cases.