@bannynet/core-v6 0.0.1
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/README.md +53 -0
- package/SKILLS.md +94 -0
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +1809 -0
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +1795 -0
- package/foundry.toml +22 -0
- package/package.json +53 -0
- package/remappings.txt +1 -0
- package/script/1.Fix.s.sol +290 -0
- package/script/Add.Denver.s.sol +75 -0
- package/script/AirdropOutfits.s.sol +2302 -0
- package/script/Deploy.s.sol +440 -0
- package/script/Drop1.s.sol +979 -0
- package/script/MigrationContractArbitrum.sol +494 -0
- package/script/MigrationContractArbitrum1.sol +170 -0
- package/script/MigrationContractArbitrum2.sol +204 -0
- package/script/MigrationContractArbitrum3.sol +174 -0
- package/script/MigrationContractArbitrum4.sol +478 -0
- package/script/MigrationContractBase1.sol +444 -0
- package/script/MigrationContractBase2.sol +175 -0
- package/script/MigrationContractBase3.sol +309 -0
- package/script/MigrationContractBase4.sol +350 -0
- package/script/MigrationContractBase5.sol +259 -0
- package/script/MigrationContractEthereum1.sol +468 -0
- package/script/MigrationContractEthereum2.sol +306 -0
- package/script/MigrationContractEthereum3.sol +349 -0
- package/script/MigrationContractEthereum4.sol +352 -0
- package/script/MigrationContractEthereum5.sol +354 -0
- package/script/MigrationContractEthereum6.sol +270 -0
- package/script/MigrationContractEthereum7.sol +439 -0
- package/script/MigrationContractEthereum8.sol +385 -0
- package/script/MigrationContractOptimism.sol +196 -0
- package/script/helpers/BannyverseDeploymentLib.sol +73 -0
- package/script/helpers/MigrationHelper.sol +155 -0
- package/script/outfit_drop/generate-migration.js +3441 -0
- package/script/outfit_drop/raw.json +43276 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/Banny721TokenUriResolver.sol +1288 -0
- package/src/interfaces/IBanny721TokenUriResolver.sol +137 -0
- package/test/Banny721TokenUriResolver.t.sol +669 -0
- package/test/BannyAttacks.t.sol +322 -0
- package/test/DecorateFlow.t.sol +1056 -0
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IERC721} from "@bananapus/721-hook-v6/src/abstract/ERC721.sol";
|
|
5
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
6
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
+
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
8
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
9
|
+
import {JBIpfsDecoder} from "@bananapus/721-hook-v6/src/libraries/JBIpfsDecoder.sol";
|
|
10
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
11
|
+
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
12
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
13
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
14
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
15
|
+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
|
|
16
|
+
import {Base64} from "lib/base64/base64.sol";
|
|
17
|
+
|
|
18
|
+
import {IBanny721TokenUriResolver} from "./interfaces/IBanny721TokenUriResolver.sol";
|
|
19
|
+
|
|
20
|
+
/// @notice Banny asset manager. Stores and shows banny bodies in backgrounds with outfits on.
|
|
21
|
+
contract Banny721TokenUriResolver is
|
|
22
|
+
Ownable,
|
|
23
|
+
ERC2771Context,
|
|
24
|
+
ReentrancyGuard,
|
|
25
|
+
IJB721TokenUriResolver,
|
|
26
|
+
IBanny721TokenUriResolver,
|
|
27
|
+
IERC721Receiver
|
|
28
|
+
{
|
|
29
|
+
using Strings for uint256;
|
|
30
|
+
|
|
31
|
+
error Banny721TokenUriResolver_CantAccelerateTheLock();
|
|
32
|
+
error Banny721TokenUriResolver_ContentsAlreadyStored();
|
|
33
|
+
error Banny721TokenUriResolver_ContentsMismatch();
|
|
34
|
+
error Banny721TokenUriResolver_HashAlreadyStored();
|
|
35
|
+
error Banny721TokenUriResolver_HashNotFound();
|
|
36
|
+
error Banny721TokenUriResolver_HeadAlreadyAdded();
|
|
37
|
+
error Banny721TokenUriResolver_OutfitChangesLocked();
|
|
38
|
+
error Banny721TokenUriResolver_SuitAlreadyAdded();
|
|
39
|
+
error Banny721TokenUriResolver_UnauthorizedBannyBody();
|
|
40
|
+
error Banny721TokenUriResolver_UnauthorizedOutfit();
|
|
41
|
+
error Banny721TokenUriResolver_UnauthorizedBackground();
|
|
42
|
+
error Banny721TokenUriResolver_UnorderedCategories();
|
|
43
|
+
error Banny721TokenUriResolver_UnrecognizedCategory();
|
|
44
|
+
error Banny721TokenUriResolver_UnrecognizedBackground();
|
|
45
|
+
error Banny721TokenUriResolver_UnrecognizedProduct();
|
|
46
|
+
error Banny721TokenUriResolver_UnauthorizedTransfer();
|
|
47
|
+
|
|
48
|
+
//*********************************************************************//
|
|
49
|
+
// ------------------------ private constants ------------------------ //
|
|
50
|
+
//*********************************************************************//
|
|
51
|
+
|
|
52
|
+
/// @notice Just a kind reminder to our readers.
|
|
53
|
+
/// @dev Used in 721 token ID generation.
|
|
54
|
+
uint256 private constant _ONE_BILLION = 1_000_000_000;
|
|
55
|
+
|
|
56
|
+
/// @notice The duration that banny bodies can be locked for.
|
|
57
|
+
uint256 private constant _LOCK_DURATION = 7 days;
|
|
58
|
+
|
|
59
|
+
uint8 private constant _BODY_CATEGORY = 0;
|
|
60
|
+
uint8 private constant _BACKGROUND_CATEGORY = 1;
|
|
61
|
+
uint8 private constant _BACKSIDE_CATEGORY = 2;
|
|
62
|
+
uint8 private constant _NECKLACE_CATEGORY = 3;
|
|
63
|
+
uint8 private constant _HEAD_CATEGORY = 4;
|
|
64
|
+
uint8 private constant _EYES_CATEGORY = 5;
|
|
65
|
+
uint8 private constant _GLASSES_CATEGORY = 6;
|
|
66
|
+
uint8 private constant _MOUTH_CATEGORY = 7;
|
|
67
|
+
uint8 private constant _LEGS_CATEGORY = 8;
|
|
68
|
+
uint8 private constant _SUIT_CATEGORY = 9;
|
|
69
|
+
uint8 private constant _SUIT_BOTTOM_CATEGORY = 10;
|
|
70
|
+
uint8 private constant _SUIT_TOP_CATEGORY = 11;
|
|
71
|
+
uint8 private constant _HEADTOP_CATEGORY = 12;
|
|
72
|
+
uint8 private constant _HAND_CATEGORY = 13;
|
|
73
|
+
uint8 private constant _SPECIAL_SUIT_CATEGORY = 14;
|
|
74
|
+
uint8 private constant _SPECIAL_LEGS_CATEGORY = 15;
|
|
75
|
+
uint8 private constant _SPECIAL_HEAD_CATEGORY = 16;
|
|
76
|
+
uint8 private constant _SPECIAL_BODY_CATEGORY = 17;
|
|
77
|
+
|
|
78
|
+
uint8 private constant ALIEN_UPC = 1;
|
|
79
|
+
uint8 private constant PINK_UPC = 2;
|
|
80
|
+
uint8 private constant ORANGE_UPC = 3;
|
|
81
|
+
uint8 private constant ORIGINAL_UPC = 4;
|
|
82
|
+
|
|
83
|
+
//*********************************************************************//
|
|
84
|
+
// --------------------- public stored properties -------------------- //
|
|
85
|
+
//*********************************************************************//
|
|
86
|
+
|
|
87
|
+
/// @notice The amount of time each banny body is currently locked for.
|
|
88
|
+
/// @custom:param hook The hook address of the collection.
|
|
89
|
+
/// @custom:param bannyBodyId The ID of the banny body to lock.
|
|
90
|
+
mapping(address hook => mapping(uint256 upc => uint256)) public override outfitLockedUntil;
|
|
91
|
+
|
|
92
|
+
/// @notice The base of the domain hosting the SVG files that can be lazily uploaded to the contract.
|
|
93
|
+
string public override svgBaseUri;
|
|
94
|
+
|
|
95
|
+
/// @notice The banny body and outfit SVG hash files.
|
|
96
|
+
/// @custom:param upc The universal product code that the SVG hash represent.
|
|
97
|
+
mapping(uint256 upc => bytes32) public override svgHashOf;
|
|
98
|
+
|
|
99
|
+
string public override DEFAULT_ALIEN_EYES;
|
|
100
|
+
string public override DEFAULT_MOUTH;
|
|
101
|
+
string public override DEFAULT_NECKLACE;
|
|
102
|
+
string public override DEFAULT_STANDARD_EYES;
|
|
103
|
+
string public override BANNY_BODY;
|
|
104
|
+
|
|
105
|
+
//*********************************************************************//
|
|
106
|
+
// --------------------- internal stored properties ------------------ //
|
|
107
|
+
//*********************************************************************//
|
|
108
|
+
|
|
109
|
+
/// @notice The outfits currently attached to each banny body.
|
|
110
|
+
/// @dev Naked Banny's will only be shown with outfits currently owned by the owner of the banny body.
|
|
111
|
+
/// @custom:param hook The hook address of the collection.
|
|
112
|
+
/// @custom:param bannyBodyId The ID of the banny body of the outfits.
|
|
113
|
+
mapping(address hook => mapping(uint256 bannyBodyId => uint256[])) internal _attachedOutfitIdsOf;
|
|
114
|
+
|
|
115
|
+
/// @notice The background currently attached to each banny body.
|
|
116
|
+
/// @dev Naked Banny's will only be shown with a background currently owned by the owner of the banny body.
|
|
117
|
+
/// @custom:param hook The hook address of the collection.
|
|
118
|
+
/// @custom:param bannyBodyId The ID of the banny body of the background.
|
|
119
|
+
mapping(address hook => mapping(uint256 bannyBodyId => uint256)) internal _attachedBackgroundIdOf;
|
|
120
|
+
|
|
121
|
+
/// @notice The name of each product.
|
|
122
|
+
/// @custom:param upc The universal product code that the name belongs to.
|
|
123
|
+
mapping(uint256 upc => string) internal _customProductNameOf;
|
|
124
|
+
|
|
125
|
+
/// @notice The banny body and outfit SVG files.
|
|
126
|
+
/// @custom:param upc The universal product code that the SVG contents represent.
|
|
127
|
+
mapping(uint256 upc => string) internal _svgContentOf;
|
|
128
|
+
|
|
129
|
+
/// @notice The ID of the banny body each background is being used by.
|
|
130
|
+
/// @custom:param hook The hook address of the collection.
|
|
131
|
+
/// @custom:param backgroundId The ID of the background.
|
|
132
|
+
mapping(address hook => mapping(uint256 backgroundId => uint256)) internal _userOf;
|
|
133
|
+
|
|
134
|
+
/// @notice The ID of the banny body each outfit is being worn by.
|
|
135
|
+
/// @custom:param hook The hook address of the collection.
|
|
136
|
+
/// @custom:param outfitId The ID of the outfit.
|
|
137
|
+
mapping(address hook => mapping(uint256 outfitId => uint256)) internal _wearerOf;
|
|
138
|
+
|
|
139
|
+
//*********************************************************************//
|
|
140
|
+
// -------------------------- constructor ---------------------------- //
|
|
141
|
+
//*********************************************************************//
|
|
142
|
+
|
|
143
|
+
/// @param bannyBody The SVG of the banny body.
|
|
144
|
+
/// @param defaultNecklace The SVG of the default necklace.
|
|
145
|
+
/// @param defaultMouth The SVG of the default mouth.
|
|
146
|
+
/// @param defaultStandardEyes The SVG of the default standard eyes.
|
|
147
|
+
/// @param defaultAlienEyes The SVG of the default alien eyes.
|
|
148
|
+
/// @param owner The owner allowed to add SVG files that correspond to product IDs.
|
|
149
|
+
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
150
|
+
constructor(
|
|
151
|
+
string memory bannyBody,
|
|
152
|
+
string memory defaultNecklace,
|
|
153
|
+
string memory defaultMouth,
|
|
154
|
+
string memory defaultStandardEyes,
|
|
155
|
+
string memory defaultAlienEyes,
|
|
156
|
+
address owner,
|
|
157
|
+
address trustedForwarder
|
|
158
|
+
)
|
|
159
|
+
Ownable(owner)
|
|
160
|
+
ERC2771Context(trustedForwarder)
|
|
161
|
+
{
|
|
162
|
+
BANNY_BODY = bannyBody;
|
|
163
|
+
DEFAULT_NECKLACE = defaultNecklace;
|
|
164
|
+
DEFAULT_MOUTH = defaultMouth;
|
|
165
|
+
DEFAULT_STANDARD_EYES = defaultStandardEyes;
|
|
166
|
+
DEFAULT_ALIEN_EYES = defaultAlienEyes;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//*********************************************************************//
|
|
170
|
+
// ------------------------- external views -------------------------- //
|
|
171
|
+
//*********************************************************************//
|
|
172
|
+
|
|
173
|
+
/// @notice Returns the SVG showing a dressed banny body in a background.
|
|
174
|
+
/// @param tokenId The ID of the token to show. If the ID belongs to a banny body, it will be shown with its
|
|
175
|
+
/// current outfits in its current background.
|
|
176
|
+
/// @return tokenUri The URI representing the SVG.
|
|
177
|
+
function tokenUriOf(address hook, uint256 tokenId) external view override returns (string memory) {
|
|
178
|
+
// Get a reference to the product for the given token ID.
|
|
179
|
+
JB721Tier memory product = _productOfTokenId({hook: hook, tokenId: tokenId});
|
|
180
|
+
|
|
181
|
+
// If the token's product ID doesn't exist, return an empty uri.
|
|
182
|
+
if (product.id == 0) return "";
|
|
183
|
+
|
|
184
|
+
string memory contents;
|
|
185
|
+
string memory extraMetadata = "";
|
|
186
|
+
string memory attributes = '"attributes": [';
|
|
187
|
+
|
|
188
|
+
// If this isn't a banny body, return the asset SVG alone (or on a manakin banny).
|
|
189
|
+
if (product.category != _BODY_CATEGORY) {
|
|
190
|
+
// Keep a reference to the SVG contents.
|
|
191
|
+
contents = _svgOf({hook: hook, upc: product.id});
|
|
192
|
+
|
|
193
|
+
// Layer the outfit SVG over the mannequin Banny
|
|
194
|
+
// Start with the mannequin SVG if we're not returning a background.
|
|
195
|
+
if (bytes(contents).length != 0) {
|
|
196
|
+
if (product.category != _BACKGROUND_CATEGORY) {
|
|
197
|
+
contents = string.concat(_mannequinBannySvg(), contents);
|
|
198
|
+
}
|
|
199
|
+
contents = _layeredSvg(contents);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// If the background or outfit is attached to a banny body, add it to extraMetadata.
|
|
203
|
+
if (product.category == _BACKGROUND_CATEGORY) {
|
|
204
|
+
uint256 bannyBodyId = userOf({hook: hook, backgroundId: tokenId});
|
|
205
|
+
extraMetadata = string.concat('"usedByBannyBodyId": ', bannyBodyId.toString(), ",");
|
|
206
|
+
attributes = string.concat(
|
|
207
|
+
attributes, '{"trait_type": "Used by Banny", "value": ', bannyBodyId.toString(), "},"
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
uint256 bannyBodyId = wearerOf({hook: hook, outfitId: tokenId});
|
|
211
|
+
extraMetadata = string.concat('"wornByBannyBodyId": ', bannyBodyId.toString(), ",");
|
|
212
|
+
attributes = string.concat(
|
|
213
|
+
attributes, '{"trait_type": "Worn by Banny", "value": ', bannyBodyId.toString(), "},"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
// Compose the contents.
|
|
218
|
+
contents = svgOf({
|
|
219
|
+
hook: hook, tokenId: tokenId, shouldDressBannyBody: true, shouldIncludeBackgroundOnBannyBody: true
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Get a reference to each asset ID currently attached to the banny body.
|
|
223
|
+
(uint256 backgroundId, uint256[] memory outfitIds) = assetIdsOf({hook: hook, bannyBodyId: tokenId});
|
|
224
|
+
|
|
225
|
+
extraMetadata = '"outfitIds": [';
|
|
226
|
+
|
|
227
|
+
for (uint256 i; i < outfitIds.length; i++) {
|
|
228
|
+
extraMetadata = string.concat(extraMetadata, outfitIds[i].toString());
|
|
229
|
+
|
|
230
|
+
// Add a comma if it's not the last outfit.
|
|
231
|
+
if (i < outfitIds.length - 1) {
|
|
232
|
+
extraMetadata = string.concat(extraMetadata, ",");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
extraMetadata = string.concat(extraMetadata, "],");
|
|
237
|
+
|
|
238
|
+
for (uint256 i; i < outfitIds.length; i++) {
|
|
239
|
+
JB721Tier memory outfitProduct = _productOfTokenId({hook: hook, tokenId: outfitIds[i]});
|
|
240
|
+
|
|
241
|
+
attributes = string.concat(
|
|
242
|
+
attributes,
|
|
243
|
+
'{"trait_type": "',
|
|
244
|
+
_categoryNameOf(outfitProduct.category),
|
|
245
|
+
'", "value": "',
|
|
246
|
+
_productNameOf(outfitProduct.id),
|
|
247
|
+
'"},'
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (backgroundId != 0) {
|
|
252
|
+
extraMetadata = string.concat(extraMetadata, '"backgroundId": ', backgroundId.toString(), ",");
|
|
253
|
+
attributes = string.concat(
|
|
254
|
+
attributes,
|
|
255
|
+
'{"trait_type": "Background", "value": "',
|
|
256
|
+
_productNameOf(_productOfTokenId({hook: hook, tokenId: backgroundId}).id),
|
|
257
|
+
'"},'
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
attributes = string.concat(attributes, '{"trait_type": "Background", "value": ""},');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// If the token has an owner, check if the owner has locked the token.
|
|
264
|
+
uint256 lockedUntil = outfitLockedUntil[hook][tokenId];
|
|
265
|
+
if (lockedUntil > block.timestamp) {
|
|
266
|
+
extraMetadata = string.concat(extraMetadata, '"changesLockedUntil": ', lockedUntil.toString(), ",");
|
|
267
|
+
attributes = string.concat(
|
|
268
|
+
attributes, '{"trait_type": "Changes locked until", "value": ', lockedUntil.toString(), "},"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (bytes(contents).length == 0) {
|
|
274
|
+
// If the product's category is greater than the last expected category, use the default base URI of the 721
|
|
275
|
+
// contract. Otherwise use the SVG URI.
|
|
276
|
+
string memory baseUri =
|
|
277
|
+
product.category > _SPECIAL_BODY_CATEGORY ? IJB721TiersHook(hook).baseURI() : svgBaseUri;
|
|
278
|
+
|
|
279
|
+
// Fallback to returning an IPFS hash if present.
|
|
280
|
+
return JBIpfsDecoder.decode({
|
|
281
|
+
baseUri: baseUri, hexString: _storeOf(hook).encodedTierIPFSUriOf({hook: hook, tokenId: tokenId})
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get a reference to the pricing context.
|
|
286
|
+
// slither-disable-next-line unused-return
|
|
287
|
+
(uint256 currency, uint256 decimals,) = IJB721TiersHook(hook).pricingContext();
|
|
288
|
+
|
|
289
|
+
attributes = string.concat(
|
|
290
|
+
attributes,
|
|
291
|
+
'{"trait_type": "Product name", "value": "',
|
|
292
|
+
_productNameOf(product.id),
|
|
293
|
+
'"}, {"trait_type": "Category name", "value": "',
|
|
294
|
+
_categoryNameOf(product.category),
|
|
295
|
+
'"}, {"trait_type": "Total supply", "value": "',
|
|
296
|
+
uint256(product.initialSupply).toString(),
|
|
297
|
+
'"}]'
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return string.concat(
|
|
301
|
+
"data:application/json;base64,",
|
|
302
|
+
Base64.encode(
|
|
303
|
+
abi.encodePacked(
|
|
304
|
+
'{"name":"',
|
|
305
|
+
_fullNameOf({tokenId: tokenId, product: product}),
|
|
306
|
+
'", "productName": "',
|
|
307
|
+
_productNameOf(product.id),
|
|
308
|
+
'", "categoryName": "',
|
|
309
|
+
_categoryNameOf(product.category),
|
|
310
|
+
'", "tokenId": ',
|
|
311
|
+
tokenId.toString(),
|
|
312
|
+
', "upc": ',
|
|
313
|
+
uint256(product.id).toString(),
|
|
314
|
+
', "category": ',
|
|
315
|
+
uint256(product.category).toString(),
|
|
316
|
+
', "supply": ',
|
|
317
|
+
uint256(product.initialSupply).toString(),
|
|
318
|
+
', "remaining": ',
|
|
319
|
+
uint256(product.remainingSupply).toString(),
|
|
320
|
+
', "price": ',
|
|
321
|
+
uint256(product.price).toString(),
|
|
322
|
+
', "decimals": ',
|
|
323
|
+
decimals.toString(),
|
|
324
|
+
', "currency": ',
|
|
325
|
+
currency.toString(),
|
|
326
|
+
', "discount": ',
|
|
327
|
+
uint256(product.discountPercent).toString(),
|
|
328
|
+
", ",
|
|
329
|
+
extraMetadata, // Keeps extraMetadata as it was
|
|
330
|
+
attributes, // Includes all attributes
|
|
331
|
+
', "description":"A piece of Banny Retail.","external_url":"https://retail.banny.eth.sucks","image":"data:image/svg+xml;base64,',
|
|
332
|
+
Base64.encode(abi.encodePacked(contents)),
|
|
333
|
+
'"}'
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//*********************************************************************//
|
|
340
|
+
// -------------------------- public views --------------------------- //
|
|
341
|
+
//*********************************************************************//
|
|
342
|
+
|
|
343
|
+
/// @notice The assets currently attached to each banny body.
|
|
344
|
+
/// @custom:param hook The hook address of the collection.
|
|
345
|
+
/// @param bannyBodyId The ID of the banny body shown with the associated assets.
|
|
346
|
+
/// @return backgroundId The background attached to the banny body.
|
|
347
|
+
/// @return outfitIds The outfits attached to the banny body.
|
|
348
|
+
function assetIdsOf(
|
|
349
|
+
address hook,
|
|
350
|
+
uint256 bannyBodyId
|
|
351
|
+
)
|
|
352
|
+
public
|
|
353
|
+
view
|
|
354
|
+
override
|
|
355
|
+
returns (uint256 backgroundId, uint256[] memory outfitIds)
|
|
356
|
+
{
|
|
357
|
+
// Keep a reference to the outfit IDs currently stored as attached to the banny body.
|
|
358
|
+
uint256[] memory storedOutfitIds = _attachedOutfitIdsOf[hook][bannyBodyId];
|
|
359
|
+
|
|
360
|
+
// Initiate the outfit IDs array with the same number of entries.
|
|
361
|
+
outfitIds = new uint256[](storedOutfitIds.length);
|
|
362
|
+
|
|
363
|
+
// Keep a reference to the number of included outfits.
|
|
364
|
+
uint256 numberOfIncludedOutfits = 0;
|
|
365
|
+
|
|
366
|
+
// Keep a reference to the stored outfit ID being iterated on.
|
|
367
|
+
uint256 storedOutfitId;
|
|
368
|
+
|
|
369
|
+
// Return the outfit's that are still being worn by the banny body.
|
|
370
|
+
for (uint256 i; i < storedOutfitIds.length; i++) {
|
|
371
|
+
// Set the stored outfit ID being iterated on.
|
|
372
|
+
storedOutfitId = storedOutfitIds[i];
|
|
373
|
+
|
|
374
|
+
// If the stored outfit is still being worn, return it.
|
|
375
|
+
if (wearerOf({hook: hook, outfitId: storedOutfitId}) == bannyBodyId) {
|
|
376
|
+
outfitIds[numberOfIncludedOutfits++] = storedOutfitId;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Resize the array to the actual number of included outfits (remove trailing zeros).
|
|
381
|
+
assembly {
|
|
382
|
+
mstore(outfitIds, numberOfIncludedOutfits)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Keep a reference to the background currently stored as attached to the banny body.
|
|
386
|
+
uint256 storedBackgroundOf = _attachedBackgroundIdOf[hook][bannyBodyId];
|
|
387
|
+
|
|
388
|
+
// If the background is still being used, return it.
|
|
389
|
+
if (userOf({hook: hook, backgroundId: storedBackgroundOf}) == bannyBodyId) backgroundId = storedBackgroundOf;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// @notice Returns the name of the token.
|
|
393
|
+
/// @param hook The hook storing the assets.
|
|
394
|
+
/// @param tokenId The ID of the token to show.
|
|
395
|
+
/// @return fullName The full name of the token.
|
|
396
|
+
/// @return categoryName The name of the token's category.
|
|
397
|
+
/// @return productName The name of the token's product.
|
|
398
|
+
function namesOf(
|
|
399
|
+
address hook,
|
|
400
|
+
uint256 tokenId
|
|
401
|
+
)
|
|
402
|
+
public
|
|
403
|
+
view
|
|
404
|
+
override
|
|
405
|
+
returns (string memory, string memory, string memory)
|
|
406
|
+
{
|
|
407
|
+
// Get a reference to the product for the given token ID.
|
|
408
|
+
JB721Tier memory product = _productOfTokenId({hook: hook, tokenId: tokenId});
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
_fullNameOf({tokenId: tokenId, product: product}),
|
|
412
|
+
_categoryNameOf(product.category),
|
|
413
|
+
_productNameOf(product.id)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// @notice Returns the SVG showing either a banny body with/without outfits and a background, or the stand alone
|
|
418
|
+
/// outfit
|
|
419
|
+
/// or background.
|
|
420
|
+
/// @param hook The hook storing the assets.
|
|
421
|
+
/// @param tokenId The ID of the token to show. If the ID belongs to a banny body, it will be shown with its
|
|
422
|
+
/// current outfits in its current background if specified.
|
|
423
|
+
/// @param shouldDressBannyBody Whether the banny body should be dressed.
|
|
424
|
+
/// @param shouldIncludeBackgroundOnBannyBody Whether the background should be included on the banny body.
|
|
425
|
+
/// @return svg The SVG.
|
|
426
|
+
function svgOf(
|
|
427
|
+
address hook,
|
|
428
|
+
uint256 tokenId,
|
|
429
|
+
bool shouldDressBannyBody,
|
|
430
|
+
bool shouldIncludeBackgroundOnBannyBody
|
|
431
|
+
)
|
|
432
|
+
public
|
|
433
|
+
view
|
|
434
|
+
override
|
|
435
|
+
returns (string memory)
|
|
436
|
+
{
|
|
437
|
+
// Get a reference to the product for the given token ID.
|
|
438
|
+
JB721Tier memory product = _productOfTokenId({hook: hook, tokenId: tokenId});
|
|
439
|
+
|
|
440
|
+
// If the token's product doesn't exist, return an empty uri.
|
|
441
|
+
if (product.id == 0) return "";
|
|
442
|
+
|
|
443
|
+
// Compose the contents.
|
|
444
|
+
string memory contents;
|
|
445
|
+
|
|
446
|
+
// If this isn't a banny body and there's an SVG available, return the asset SVG alone.
|
|
447
|
+
if (product.category != _BODY_CATEGORY) {
|
|
448
|
+
// Keep a reference to the SVG contents.
|
|
449
|
+
contents = _svgOf({hook: hook, upc: product.id});
|
|
450
|
+
|
|
451
|
+
// Return the svg if it exists.
|
|
452
|
+
return (bytes(contents).length == 0) ? "" : _layeredSvg(contents);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Get a reference to each asset ID currently attached to the banny body.
|
|
456
|
+
(uint256 backgroundId, uint256[] memory outfitIds) = assetIdsOf({hook: hook, bannyBodyId: tokenId});
|
|
457
|
+
|
|
458
|
+
// Add the background if needed.
|
|
459
|
+
if (backgroundId != 0 && shouldIncludeBackgroundOnBannyBody) {
|
|
460
|
+
contents = string.concat(
|
|
461
|
+
contents, _svgOf({hook: hook, upc: _productOfTokenId({hook: hook, tokenId: backgroundId}).id})
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Start with the banny body.
|
|
466
|
+
contents = string.concat(contents, _bannyBodySvgOf({upc: product.id}));
|
|
467
|
+
|
|
468
|
+
if (shouldDressBannyBody) {
|
|
469
|
+
// Get the outfit contents.
|
|
470
|
+
string memory outfitContents = _outfitContentsFor({hook: hook, outfitIds: outfitIds, bodyUpc: product.id});
|
|
471
|
+
|
|
472
|
+
// Add the outfit contents if there are any.
|
|
473
|
+
if (bytes(outfitContents).length != 0) {
|
|
474
|
+
contents = string.concat(contents, outfitContents);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Return the SVG contents.
|
|
479
|
+
return _layeredSvg(contents);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/// @notice Checks to see which banny body is currently using a particular background.
|
|
483
|
+
/// @param hook The hook address of the collection.
|
|
484
|
+
/// @param backgroundId The ID of the background being used.
|
|
485
|
+
/// @return The ID of the banny body using the background.
|
|
486
|
+
function userOf(address hook, uint256 backgroundId) public view override returns (uint256) {
|
|
487
|
+
// Get a reference to the banny body using the background.
|
|
488
|
+
uint256 bannyBodyId = _userOf[hook][backgroundId];
|
|
489
|
+
|
|
490
|
+
// If no banny body is wearing the outfit, or if its no longer the background attached, return 0.
|
|
491
|
+
if (bannyBodyId == 0 || _attachedBackgroundIdOf[hook][bannyBodyId] != backgroundId) return 0;
|
|
492
|
+
|
|
493
|
+
// Return the banny body ID.
|
|
494
|
+
return bannyBodyId;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// @notice Checks to see which banny body is currently wearing a particular outfit.
|
|
498
|
+
/// @param hook The hook address of the collection.
|
|
499
|
+
/// @param outfitId The ID of the outfit being worn.
|
|
500
|
+
/// @return The ID of the banny body wearing the outfit.
|
|
501
|
+
function wearerOf(address hook, uint256 outfitId) public view override returns (uint256) {
|
|
502
|
+
// Get a reference to the banny body wearing the outfit.
|
|
503
|
+
uint256 bannyBodyId = _wearerOf[hook][outfitId];
|
|
504
|
+
|
|
505
|
+
// If no banny body is wearing the outfit, return 0.
|
|
506
|
+
if (bannyBodyId == 0) return 0;
|
|
507
|
+
|
|
508
|
+
// Keep a reference to the outfit IDs currently attached to a banny body.
|
|
509
|
+
uint256[] memory attachedOutfitIds = _attachedOutfitIdsOf[hook][bannyBodyId];
|
|
510
|
+
|
|
511
|
+
for (uint256 i; i < attachedOutfitIds.length; i++) {
|
|
512
|
+
// If the outfit is still attached, return the banny body ID.
|
|
513
|
+
if (attachedOutfitIds[i] == outfitId) return bannyBodyId;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// If the outfit is no longer attached, return 0.
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
//*********************************************************************//
|
|
521
|
+
// -------------------------- internal views ------------------------- //
|
|
522
|
+
//*********************************************************************//
|
|
523
|
+
|
|
524
|
+
/// @notice The name of each token's category.
|
|
525
|
+
/// @param category The category of the token being named.
|
|
526
|
+
/// @return name The token's category name.
|
|
527
|
+
function _categoryNameOf(uint256 category) internal pure returns (string memory) {
|
|
528
|
+
if (category == _BODY_CATEGORY) {
|
|
529
|
+
return "Banny body";
|
|
530
|
+
} else if (category == _BACKGROUND_CATEGORY) {
|
|
531
|
+
return "Background";
|
|
532
|
+
} else if (category == _BACKSIDE_CATEGORY) {
|
|
533
|
+
return "Backside";
|
|
534
|
+
} else if (category == _LEGS_CATEGORY) {
|
|
535
|
+
return "Legs";
|
|
536
|
+
} else if (category == _NECKLACE_CATEGORY) {
|
|
537
|
+
return "Necklace";
|
|
538
|
+
} else if (category == _EYES_CATEGORY) {
|
|
539
|
+
return "Eyes";
|
|
540
|
+
} else if (category == _GLASSES_CATEGORY) {
|
|
541
|
+
return "Glasses";
|
|
542
|
+
} else if (category == _MOUTH_CATEGORY) {
|
|
543
|
+
return "Mouth";
|
|
544
|
+
} else if (category == _HEADTOP_CATEGORY) {
|
|
545
|
+
return "Head top";
|
|
546
|
+
} else if (category == _HEAD_CATEGORY) {
|
|
547
|
+
return "Head";
|
|
548
|
+
} else if (category == _SUIT_CATEGORY) {
|
|
549
|
+
return "Suit";
|
|
550
|
+
} else if (category == _SUIT_TOP_CATEGORY) {
|
|
551
|
+
return "Suit top";
|
|
552
|
+
} else if (category == _SUIT_BOTTOM_CATEGORY) {
|
|
553
|
+
return "Suit bottom";
|
|
554
|
+
} else if (category == _HAND_CATEGORY) {
|
|
555
|
+
return "Fist";
|
|
556
|
+
} else if (category == _SPECIAL_SUIT_CATEGORY) {
|
|
557
|
+
return "Special Suit";
|
|
558
|
+
} else if (category == _SPECIAL_LEGS_CATEGORY) {
|
|
559
|
+
return "Special Legs";
|
|
560
|
+
} else if (category == _SPECIAL_HEAD_CATEGORY) {
|
|
561
|
+
return "Special Head";
|
|
562
|
+
} else if (category == _SPECIAL_BODY_CATEGORY) {
|
|
563
|
+
return "Special Body";
|
|
564
|
+
}
|
|
565
|
+
return "";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
569
|
+
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
570
|
+
return super._contextSuffixLength();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// @notice Make sure the message sender own's the token.
|
|
574
|
+
/// @param hook The 721 contract of the token having ownership checked.
|
|
575
|
+
/// @param upc The product's UPC to check ownership of.
|
|
576
|
+
function _checkIfSenderIsOwner(address hook, uint256 upc) internal view {
|
|
577
|
+
if (IERC721(hook).ownerOf(upc) != _msgSender()) revert Banny721TokenUriResolver_UnauthorizedBannyBody();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// @notice The fills for a product.
|
|
581
|
+
/// @param upc The ID of the token whose product's fills are being returned.
|
|
582
|
+
/// @return fills The fills for the product.
|
|
583
|
+
function _fillsFor(uint256 upc)
|
|
584
|
+
internal
|
|
585
|
+
pure
|
|
586
|
+
returns (
|
|
587
|
+
string memory,
|
|
588
|
+
string memory,
|
|
589
|
+
string memory,
|
|
590
|
+
string memory,
|
|
591
|
+
string memory,
|
|
592
|
+
string memory,
|
|
593
|
+
string memory
|
|
594
|
+
)
|
|
595
|
+
{
|
|
596
|
+
if (upc == ALIEN_UPC) {
|
|
597
|
+
return ("67d757", "30a220", "217a15", "09490f", "e483ef", "dc2fef", "dc2fef");
|
|
598
|
+
} else if (upc == PINK_UPC) {
|
|
599
|
+
return ("ffd8c5", "ff96a9", "fe588b", "c92f45", "ffd8c5", "ff96a9", "fe588b");
|
|
600
|
+
} else if (upc == ORANGE_UPC) {
|
|
601
|
+
return ("f3a603", "ff7c02", "fd3600", "c32e0d", "f3a603", "ff7c02", "fd3600");
|
|
602
|
+
} else if (upc == ORIGINAL_UPC) {
|
|
603
|
+
return ("ffe900", "ffc700", "f3a603", "965a1a", "ffe900", "ffc700", "f3a603");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
revert Banny721TokenUriResolver_UnrecognizedProduct();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// @notice The full name of each product, including category and inventory.
|
|
610
|
+
/// @param tokenId The ID of the token being named.
|
|
611
|
+
/// @param product The product of the token being named.
|
|
612
|
+
/// @return name The full name.
|
|
613
|
+
function _fullNameOf(uint256 tokenId, JB721Tier memory product) internal view returns (string memory name) {
|
|
614
|
+
// Start with the item's name.
|
|
615
|
+
name = string.concat(_productNameOf(product.id), " ");
|
|
616
|
+
|
|
617
|
+
// Get just the token ID without the product ID included.
|
|
618
|
+
uint256 rawTokenId = tokenId % _ONE_BILLION;
|
|
619
|
+
|
|
620
|
+
string memory remainingString = " remaining";
|
|
621
|
+
|
|
622
|
+
// If there's a raw token id, append it to the name before appending it to the category.
|
|
623
|
+
if (rawTokenId != 0) {
|
|
624
|
+
name = string.concat(name, rawTokenId.toString(), "/", uint256(product.initialSupply).toString());
|
|
625
|
+
} else if (product.remainingSupply == 0) {
|
|
626
|
+
name = string.concat(
|
|
627
|
+
name,
|
|
628
|
+
" (SOLD OUT) ",
|
|
629
|
+
uint256(product.remainingSupply).toString(),
|
|
630
|
+
"/",
|
|
631
|
+
uint256(product.initialSupply).toString(),
|
|
632
|
+
remainingString
|
|
633
|
+
);
|
|
634
|
+
} else {
|
|
635
|
+
name = string.concat(
|
|
636
|
+
name,
|
|
637
|
+
uint256(product.remainingSupply).toString(),
|
|
638
|
+
"/",
|
|
639
|
+
uint256(product.initialSupply).toString(),
|
|
640
|
+
remainingString
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Append a separator.
|
|
645
|
+
name = string.concat(name, ": ");
|
|
646
|
+
|
|
647
|
+
// Get a reference to the category's name.
|
|
648
|
+
string memory categoryName = _categoryNameOf(product.category);
|
|
649
|
+
|
|
650
|
+
// If there's a category name, append it.
|
|
651
|
+
if (bytes(categoryName).length != 0) {
|
|
652
|
+
name = string.concat(name, categoryName, " ");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Append the product ID as a universal product code.
|
|
656
|
+
name = string.concat(name, "UPC #", uint256(product.id).toString());
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// @notice Returns the standard dimension SVG containing dynamic contents and SVG metadata.
|
|
660
|
+
/// @param contents The contents of the SVG
|
|
661
|
+
/// @return svg The SVG contents.
|
|
662
|
+
function _layeredSvg(string memory contents) internal pure returns (string memory) {
|
|
663
|
+
return string.concat(
|
|
664
|
+
'<svg width="400" height="400" viewBox="0 0 400 400" fill="white" xmlns="http://www.w3.org/2000/svg"><style>.o{fill:#050505;}.w{fill:#f9f9f9;}</style>',
|
|
665
|
+
contents,
|
|
666
|
+
"</svg>"
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/// @notice The SVG contents for a mannequin banny.
|
|
671
|
+
/// @return contents The SVG contents of the mannequin banny.
|
|
672
|
+
function _mannequinBannySvg() internal view returns (string memory) {
|
|
673
|
+
string memory fillNoneString = string.concat("{fill:none;}");
|
|
674
|
+
return string.concat(
|
|
675
|
+
"<style>.o{fill:#808080;}.b1",
|
|
676
|
+
fillNoneString,
|
|
677
|
+
".b2",
|
|
678
|
+
fillNoneString,
|
|
679
|
+
".b3",
|
|
680
|
+
fillNoneString,
|
|
681
|
+
".b4",
|
|
682
|
+
fillNoneString,
|
|
683
|
+
".a1",
|
|
684
|
+
fillNoneString,
|
|
685
|
+
".a2",
|
|
686
|
+
fillNoneString,
|
|
687
|
+
".a3",
|
|
688
|
+
fillNoneString,
|
|
689
|
+
"</style>",
|
|
690
|
+
BANNY_BODY
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/// @notice Returns the calldata, preferred to use over `msg.data`
|
|
695
|
+
/// @return calldata the `msg.data` of this call
|
|
696
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
697
|
+
return ERC2771Context._msgData();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/// @notice Returns the sender, preferred to use over `msg.sender`
|
|
701
|
+
/// @return sender the sender address of this call.
|
|
702
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
703
|
+
return ERC2771Context._msgSender();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// @notice The SVG contents for a banny body.
|
|
707
|
+
/// @param upc The ID of the token whose product's SVG is being returned.
|
|
708
|
+
/// @return contents The SVG contents of the banny body.
|
|
709
|
+
function _bannyBodySvgOf(uint256 upc) internal view returns (string memory) {
|
|
710
|
+
(
|
|
711
|
+
string memory b1,
|
|
712
|
+
string memory b2,
|
|
713
|
+
string memory b3,
|
|
714
|
+
string memory b4,
|
|
715
|
+
string memory a1,
|
|
716
|
+
string memory a2,
|
|
717
|
+
string memory a3
|
|
718
|
+
) = _fillsFor(upc);
|
|
719
|
+
return string.concat(
|
|
720
|
+
"<style>.b1{fill:#",
|
|
721
|
+
b1,
|
|
722
|
+
";}.b2{fill:#",
|
|
723
|
+
b2,
|
|
724
|
+
";}.b3{fill:#",
|
|
725
|
+
b3,
|
|
726
|
+
";}.b4{fill:#",
|
|
727
|
+
b4,
|
|
728
|
+
";}.a1{fill:#",
|
|
729
|
+
a1,
|
|
730
|
+
";}.a2{fill:#",
|
|
731
|
+
a2,
|
|
732
|
+
";}.a3{fill:#",
|
|
733
|
+
a3,
|
|
734
|
+
";}</style>",
|
|
735
|
+
BANNY_BODY
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/// @notice The SVG contents for a list of outfit IDs.
|
|
740
|
+
/// @param hook The 721 contract that the product belongs to.
|
|
741
|
+
/// @param outfitIds The IDs of the outfits that'll be associated with the specified banny.
|
|
742
|
+
/// @param bodyUpc The UPC of the banny body being dressed (used for default eyes selection).
|
|
743
|
+
/// @return contents The SVG contents of the outfits.
|
|
744
|
+
function _outfitContentsFor(
|
|
745
|
+
address hook,
|
|
746
|
+
uint256[] memory outfitIds,
|
|
747
|
+
uint256 bodyUpc
|
|
748
|
+
)
|
|
749
|
+
internal
|
|
750
|
+
view
|
|
751
|
+
returns (string memory contents)
|
|
752
|
+
{
|
|
753
|
+
// Get a reference to the number of outfits are on the banny body.
|
|
754
|
+
uint256 numberOfOutfits = outfitIds.length;
|
|
755
|
+
|
|
756
|
+
// Keep a reference to if certain accessories have been added.
|
|
757
|
+
bool hasNecklace;
|
|
758
|
+
bool hasHead;
|
|
759
|
+
bool hasEyes;
|
|
760
|
+
bool hasMouth;
|
|
761
|
+
|
|
762
|
+
// Keep a reference to the custom necklace. Needed because the custom necklace is layered differently than the
|
|
763
|
+
// default.
|
|
764
|
+
string memory customNecklace;
|
|
765
|
+
|
|
766
|
+
// For each outfit, add the SVG layer if it's owned by the same owner as the banny body being dressed.
|
|
767
|
+
// Loop once more to make sure all default outfits are added.
|
|
768
|
+
for (uint256 i; i < numberOfOutfits + 1; i++) {
|
|
769
|
+
// Keep a reference to the outfit ID being iterated on.
|
|
770
|
+
uint256 outfitId;
|
|
771
|
+
|
|
772
|
+
// Keep a reference to the category of the outfit being iterated on.
|
|
773
|
+
uint256 category;
|
|
774
|
+
|
|
775
|
+
// Keep a reference to the upc of the outfit being iterated on.
|
|
776
|
+
uint256 upc;
|
|
777
|
+
|
|
778
|
+
// If the outfit is within the bounds of the number of outfits there are, add it normally.
|
|
779
|
+
if (i < numberOfOutfits) {
|
|
780
|
+
// Set the outfit ID being iterated on.
|
|
781
|
+
outfitId = outfitIds[i];
|
|
782
|
+
|
|
783
|
+
// Get the product of the outfit being iterated on.
|
|
784
|
+
JB721Tier memory product = _productOfTokenId({hook: hook, tokenId: outfitId});
|
|
785
|
+
|
|
786
|
+
// Set the category of the outfit being iterated on.
|
|
787
|
+
category = product.category;
|
|
788
|
+
|
|
789
|
+
// Set the upc of the outfit being iterated on.
|
|
790
|
+
upc = product.id;
|
|
791
|
+
} else {
|
|
792
|
+
// Set the category to be more than all other categories to force adding defaults.
|
|
793
|
+
category = _SPECIAL_BODY_CATEGORY + 1;
|
|
794
|
+
outfitId = 0;
|
|
795
|
+
upc = 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (category == _NECKLACE_CATEGORY) {
|
|
799
|
+
hasNecklace = true;
|
|
800
|
+
customNecklace = _svgOf({hook: hook, upc: upc});
|
|
801
|
+
} else if (category > _NECKLACE_CATEGORY && !hasNecklace) {
|
|
802
|
+
contents = string.concat(contents, DEFAULT_NECKLACE);
|
|
803
|
+
hasNecklace = true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (category == _HEAD_CATEGORY) {
|
|
807
|
+
hasHead = true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (category == _EYES_CATEGORY) {
|
|
811
|
+
hasEyes = true;
|
|
812
|
+
} else if (category > _EYES_CATEGORY && !hasEyes && !hasHead) {
|
|
813
|
+
if (bodyUpc == ALIEN_UPC) contents = string.concat(contents, DEFAULT_ALIEN_EYES);
|
|
814
|
+
else contents = string.concat(contents, DEFAULT_STANDARD_EYES);
|
|
815
|
+
|
|
816
|
+
hasEyes = true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (category == _MOUTH_CATEGORY) {
|
|
820
|
+
hasMouth = true;
|
|
821
|
+
} else if (category > _MOUTH_CATEGORY && !hasMouth && !hasHead) {
|
|
822
|
+
contents = string.concat(contents, DEFAULT_MOUTH);
|
|
823
|
+
hasMouth = true;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Add the custom necklace if needed.
|
|
827
|
+
if (category > _SUIT_TOP_CATEGORY && bytes(customNecklace).length != 0) {
|
|
828
|
+
contents = string.concat(contents, customNecklace);
|
|
829
|
+
// Reset.
|
|
830
|
+
customNecklace = "";
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Add the outfit if needed.
|
|
834
|
+
if (outfitId != 0 && category != _NECKLACE_CATEGORY) {
|
|
835
|
+
contents = string.concat(contents, _svgOf({hook: hook, upc: upc}));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/// @notice The name of each token's product type.
|
|
841
|
+
/// @param upc The ID of the token whose product type is being named.
|
|
842
|
+
/// @return name The item's product name.
|
|
843
|
+
function _productNameOf(uint256 upc) internal view returns (string memory) {
|
|
844
|
+
// Get the token's name.
|
|
845
|
+
if (upc == ALIEN_UPC) {
|
|
846
|
+
return "Alien";
|
|
847
|
+
} else if (upc == PINK_UPC) {
|
|
848
|
+
return "Pink";
|
|
849
|
+
} else if (upc == ORANGE_UPC) {
|
|
850
|
+
return "Orange";
|
|
851
|
+
} else if (upc == ORIGINAL_UPC) {
|
|
852
|
+
return "Original";
|
|
853
|
+
} else {
|
|
854
|
+
// Get the product's name that has been uploaded.
|
|
855
|
+
return _customProductNameOf[upc];
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/// @notice Get the product of the 721 with the provided token ID in the provided 721 contract.
|
|
860
|
+
/// @param hook The 721 contract that the product belongs to.
|
|
861
|
+
/// @param tokenId The token ID of the 721 to get the product of.
|
|
862
|
+
/// @return product The product.
|
|
863
|
+
function _productOfTokenId(address hook, uint256 tokenId) internal view returns (JB721Tier memory) {
|
|
864
|
+
return _storeOf(hook).tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/// @notice The store of the hook.
|
|
868
|
+
/// @param hook The hook to get the store of.
|
|
869
|
+
/// @return store The store of the hook.
|
|
870
|
+
function _storeOf(address hook) internal view returns (IJB721TiersHookStore) {
|
|
871
|
+
return IJB721TiersHook(hook).STORE();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/// @notice The banny body and outfit SVG files.
|
|
875
|
+
/// @param hook The 721 contract that the product belongs to.
|
|
876
|
+
/// @param upc The universal product code of the product that the SVG contents represent.
|
|
877
|
+
function _svgOf(address hook, uint256 upc) internal view returns (string memory) {
|
|
878
|
+
// Keep a reference to the stored svg contents.
|
|
879
|
+
string memory svgContents = _svgContentOf[upc];
|
|
880
|
+
|
|
881
|
+
if (bytes(svgContents).length != 0) return svgContents;
|
|
882
|
+
|
|
883
|
+
return string.concat(
|
|
884
|
+
'<image href="',
|
|
885
|
+
JBIpfsDecoder.decode({
|
|
886
|
+
baseUri: svgBaseUri, hexString: _storeOf(hook).encodedIPFSUriOf({hook: hook, tierId: upc})
|
|
887
|
+
}),
|
|
888
|
+
'" width="400" height="400"/>'
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
//*********************************************************************//
|
|
893
|
+
// ---------------------- external transactions ---------------------- //
|
|
894
|
+
//*********************************************************************//
|
|
895
|
+
|
|
896
|
+
/// @notice Dress your banny body with outfits and a background.
|
|
897
|
+
/// @dev Decoration is allowed when ALL of the following hold:
|
|
898
|
+
///
|
|
899
|
+
/// 1. The caller owns the banny body (via `_checkIfSenderIsOwner`).
|
|
900
|
+
/// 2. The banny body is not currently locked (`outfitLockedUntil` has not yet passed).
|
|
901
|
+
/// 3. For each outfit supplied:
|
|
902
|
+
/// a. The caller is the outfit's current owner, OR
|
|
903
|
+
/// b. The outfit is currently worn by another banny body and the caller owns that banny body.
|
|
904
|
+
/// (If the outfit is unworn, only (a) applies — the outfit owner must be the caller.)
|
|
905
|
+
/// 4. For the background supplied (if non-zero):
|
|
906
|
+
/// a. The caller is the background's current owner, OR
|
|
907
|
+
/// b. The background is currently used by another banny body and the caller owns that banny body.
|
|
908
|
+
/// (If the background is unused, only (a) applies — the background owner must be the caller.)
|
|
909
|
+
/// 5. Outfit categories must be valid (within recognized range) and passed in ascending order.
|
|
910
|
+
/// 6. Conflicting categories are rejected (e.g., a full head blocks individual face pieces;
|
|
911
|
+
/// a full suit blocks separate top/bottom).
|
|
912
|
+
///
|
|
913
|
+
/// @param hook The hook storing the assets.
|
|
914
|
+
/// @param bannyBodyId The ID of the banny body being dressed.
|
|
915
|
+
/// @param backgroundId The ID of the background that'll be associated with the specified banny.
|
|
916
|
+
/// @param outfitIds The IDs of the outfits that'll be associated with the specified banny. Only one outfit per
|
|
917
|
+
/// outfit category allowed at a time and they must be passed in order.
|
|
918
|
+
function decorateBannyWith(
|
|
919
|
+
address hook,
|
|
920
|
+
uint256 bannyBodyId,
|
|
921
|
+
uint256 backgroundId,
|
|
922
|
+
uint256[] calldata outfitIds
|
|
923
|
+
)
|
|
924
|
+
external
|
|
925
|
+
override
|
|
926
|
+
nonReentrant
|
|
927
|
+
{
|
|
928
|
+
_checkIfSenderIsOwner({hook: hook, upc: bannyBodyId});
|
|
929
|
+
|
|
930
|
+
// Can't decorate a banny that's locked.
|
|
931
|
+
if (outfitLockedUntil[hook][bannyBodyId] > block.timestamp) {
|
|
932
|
+
revert Banny721TokenUriResolver_OutfitChangesLocked();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
emit DecorateBanny({
|
|
936
|
+
hook: hook, bannyBodyId: bannyBodyId, backgroundId: backgroundId, outfitIds: outfitIds, caller: _msgSender()
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Add the background.
|
|
940
|
+
_decorateBannyWithBackground({hook: hook, bannyBodyId: bannyBodyId, backgroundId: backgroundId});
|
|
941
|
+
|
|
942
|
+
// Add the outfits.
|
|
943
|
+
_decorateBannyWithOutfits({hook: hook, bannyBodyId: bannyBodyId, outfitIds: outfitIds});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/// @notice Locks a banny body ID so that it can't change its outfit for a period of time.
|
|
947
|
+
/// @param hook The hook address of the collection.
|
|
948
|
+
/// @param bannyBodyId The ID of the banny body to lock.
|
|
949
|
+
function lockOutfitChangesFor(address hook, uint256 bannyBodyId) public override {
|
|
950
|
+
// Make sure only the banny body's owner can lock it.
|
|
951
|
+
_checkIfSenderIsOwner({hook: hook, upc: bannyBodyId});
|
|
952
|
+
|
|
953
|
+
// Keep a reference to the current lock.
|
|
954
|
+
uint256 currentLockedUntil = outfitLockedUntil[hook][bannyBodyId];
|
|
955
|
+
|
|
956
|
+
// Calculate the new time at which the lock will expire.
|
|
957
|
+
uint256 newLockUntil = block.timestamp + _LOCK_DURATION;
|
|
958
|
+
|
|
959
|
+
// Make sure the new lock is at least as big as the current lock.
|
|
960
|
+
if (currentLockedUntil > newLockUntil) revert Banny721TokenUriResolver_CantAccelerateTheLock();
|
|
961
|
+
|
|
962
|
+
// Set the lock.
|
|
963
|
+
outfitLockedUntil[hook][bannyBodyId] = newLockUntil;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/// @dev Make sure tokens can be received if the transaction was initiated by this contract.
|
|
967
|
+
/// @param operator The address that initiated the transaction.
|
|
968
|
+
/// @param from The address that initiated the transfer.
|
|
969
|
+
/// @param tokenId The ID of the token being transferred.
|
|
970
|
+
/// @param data The data of the transfer.
|
|
971
|
+
function onERC721Received(
|
|
972
|
+
address operator,
|
|
973
|
+
address from,
|
|
974
|
+
uint256 tokenId,
|
|
975
|
+
bytes calldata data
|
|
976
|
+
)
|
|
977
|
+
external
|
|
978
|
+
view
|
|
979
|
+
override
|
|
980
|
+
returns (bytes4)
|
|
981
|
+
{
|
|
982
|
+
from; // unused.
|
|
983
|
+
tokenId; // unused.
|
|
984
|
+
data; // unused.
|
|
985
|
+
|
|
986
|
+
// Make sure the transaction's operator is this contract.
|
|
987
|
+
if (operator != address(this)) revert Banny721TokenUriResolver_UnauthorizedTransfer();
|
|
988
|
+
|
|
989
|
+
return IERC721Receiver.onERC721Received.selector;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/// @notice Allows the owner to set the product's name.
|
|
993
|
+
/// @param upcs The universal product codes of the products having their name stored.
|
|
994
|
+
/// @param names The names of the products.
|
|
995
|
+
function setProductNames(uint256[] memory upcs, string[] memory names) external override onlyOwner {
|
|
996
|
+
for (uint256 i; i < upcs.length; i++) {
|
|
997
|
+
uint256 upc = upcs[i];
|
|
998
|
+
string memory name = names[i];
|
|
999
|
+
|
|
1000
|
+
_customProductNameOf[upc] = name;
|
|
1001
|
+
|
|
1002
|
+
emit SetProductName({upc: upc, name: name, caller: msg.sender});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/// @notice Allows the owner of this contract to specify the base of the domain hosting the SVG files.
|
|
1007
|
+
/// @param baseUri The base URI of the SVG files.
|
|
1008
|
+
function setSvgBaseUri(string calldata baseUri) external override onlyOwner {
|
|
1009
|
+
// Store the base URI.
|
|
1010
|
+
svgBaseUri = baseUri;
|
|
1011
|
+
|
|
1012
|
+
emit SetSvgBaseUri({baseUri: baseUri, caller: msg.sender});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/// @notice The owner of this contract can store SVG files for product IDs.
|
|
1016
|
+
/// @param upcs The universal product codes of the products having SVGs stored.
|
|
1017
|
+
/// @param svgContents The svg contents being stored, not including the parent <svg></svg> element.
|
|
1018
|
+
function setSvgContentsOf(uint256[] memory upcs, string[] calldata svgContents) external override {
|
|
1019
|
+
for (uint256 i; i < upcs.length; i++) {
|
|
1020
|
+
uint256 upc = upcs[i];
|
|
1021
|
+
string memory svgContent = svgContents[i];
|
|
1022
|
+
|
|
1023
|
+
// Make sure there isn't already contents for the specified universal product code.
|
|
1024
|
+
if (bytes(_svgContentOf[upc]).length != 0) revert Banny721TokenUriResolver_ContentsAlreadyStored();
|
|
1025
|
+
|
|
1026
|
+
// Get the stored svg hash for the product.
|
|
1027
|
+
bytes32 svgHash = svgHashOf[upc];
|
|
1028
|
+
|
|
1029
|
+
// Make sure a hash exists.
|
|
1030
|
+
if (svgHash == bytes32(0)) revert Banny721TokenUriResolver_HashNotFound();
|
|
1031
|
+
|
|
1032
|
+
// Make sure the content matches the hash.
|
|
1033
|
+
if (keccak256(abi.encodePacked(svgContent)) != svgHash) revert Banny721TokenUriResolver_ContentsMismatch();
|
|
1034
|
+
|
|
1035
|
+
// Store the svg contents.
|
|
1036
|
+
_svgContentOf[upc] = svgContent;
|
|
1037
|
+
|
|
1038
|
+
emit SetSvgContent({upc: upc, svgContent: svgContent, caller: msg.sender});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/// @notice Allows the owner of this contract to upload the hash of an svg file for a universal product code.
|
|
1043
|
+
/// @dev This allows anyone to lazily upload the correct svg file.
|
|
1044
|
+
/// @param upcs The universal product codes of the products having SVG hashes stored.
|
|
1045
|
+
/// @param svgHashes The svg hashes being stored, not including the parent <svg></svg> element.
|
|
1046
|
+
function setSvgHashesOf(uint256[] memory upcs, bytes32[] memory svgHashes) external override onlyOwner {
|
|
1047
|
+
for (uint256 i; i < upcs.length; i++) {
|
|
1048
|
+
uint256 upc = upcs[i];
|
|
1049
|
+
bytes32 svgHash = svgHashes[i];
|
|
1050
|
+
|
|
1051
|
+
// Make sure there isn't already contents for the specified universal product code.
|
|
1052
|
+
if (svgHashOf[upc] != bytes32(0)) revert Banny721TokenUriResolver_HashAlreadyStored();
|
|
1053
|
+
|
|
1054
|
+
// Store the svg contents.
|
|
1055
|
+
svgHashOf[upc] = svgHash;
|
|
1056
|
+
|
|
1057
|
+
emit SetSvgHash({upc: upc, svgHash: svgHash, caller: msg.sender});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
//*********************************************************************//
|
|
1062
|
+
// ---------------------- internal transactions ---------------------- //
|
|
1063
|
+
//*********************************************************************//
|
|
1064
|
+
|
|
1065
|
+
/// @notice Add outfits to a banny body.
|
|
1066
|
+
/// @dev The caller must own the banny body being dressed and all outfits being worn.
|
|
1067
|
+
/// @param hook The hook storing the assets.
|
|
1068
|
+
/// @param bannyBodyId The ID of the banny body being dressed.
|
|
1069
|
+
/// @param outfitIds The IDs of the outfits that'll be associated with the specified banny. Only one outfit per
|
|
1070
|
+
/// outfit category allowed at a time and they must be passed in order.
|
|
1071
|
+
function _decorateBannyWithOutfits(address hook, uint256 bannyBodyId, uint256[] memory outfitIds) internal {
|
|
1072
|
+
// Keep track of certain outfits being used along the way to prevent conflicting outfits.
|
|
1073
|
+
bool hasHead;
|
|
1074
|
+
bool hasSuit;
|
|
1075
|
+
|
|
1076
|
+
// Keep a reference to the category of the last outfit iterated on.
|
|
1077
|
+
uint256 lastAssetCategory;
|
|
1078
|
+
|
|
1079
|
+
// Keep a reference to the currently attached outfits on the banny body.
|
|
1080
|
+
uint256[] memory previousOutfitIds = _attachedOutfitIdsOf[hook][bannyBodyId];
|
|
1081
|
+
|
|
1082
|
+
// Keep a index counter that'll help with tracking progress.
|
|
1083
|
+
uint256 previousOutfitIndex;
|
|
1084
|
+
|
|
1085
|
+
// Keep a reference to the previous outfit being iterated on when removing.
|
|
1086
|
+
uint256 previousOutfitId;
|
|
1087
|
+
|
|
1088
|
+
// Get the outfit's product info.
|
|
1089
|
+
uint256 previousOutfitProductCategory;
|
|
1090
|
+
|
|
1091
|
+
// Set the previous values if there are previous outfits.
|
|
1092
|
+
if (previousOutfitIds.length > 0) {
|
|
1093
|
+
previousOutfitId = previousOutfitIds[0];
|
|
1094
|
+
previousOutfitProductCategory = _productOfTokenId({hook: hook, tokenId: previousOutfitId}).category;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Iterate through each outfit, transfering them in and adding them to the banny if needed, while transfering
|
|
1098
|
+
// out and removing old outfits no longer being worn.
|
|
1099
|
+
for (uint256 i; i < outfitIds.length; i++) {
|
|
1100
|
+
// Set the outfit ID being iterated on.
|
|
1101
|
+
uint256 outfitId = outfitIds[i];
|
|
1102
|
+
|
|
1103
|
+
// Keep a reference to the outfit's owner.
|
|
1104
|
+
address owner = IERC721(hook).ownerOf(outfitId);
|
|
1105
|
+
|
|
1106
|
+
// Check if the call is being made either by the outfit's owner or the owner of the banny body currently
|
|
1107
|
+
// wearing it.
|
|
1108
|
+
if (_msgSender() != owner) {
|
|
1109
|
+
// Get the banny body currently wearing this outfit.
|
|
1110
|
+
uint256 wearerId = wearerOf({hook: hook, outfitId: outfitId});
|
|
1111
|
+
|
|
1112
|
+
// If the outfit is not currently worn, only the outfit's owner can use it for decoration.
|
|
1113
|
+
if (wearerId == 0) revert Banny721TokenUriResolver_UnauthorizedOutfit();
|
|
1114
|
+
|
|
1115
|
+
// If the outfit is worn, the banny body's owner can also authorize its use.
|
|
1116
|
+
if (_msgSender() != IERC721(hook).ownerOf(wearerId)) {
|
|
1117
|
+
revert Banny721TokenUriResolver_UnauthorizedOutfit();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Get the outfit's product info.
|
|
1122
|
+
uint256 outfitProductCategory = _productOfTokenId({hook: hook, tokenId: outfitId}).category;
|
|
1123
|
+
|
|
1124
|
+
// The product's category must be a known category.
|
|
1125
|
+
if (outfitProductCategory < _BACKSIDE_CATEGORY || outfitProductCategory > _SPECIAL_BODY_CATEGORY) {
|
|
1126
|
+
revert Banny721TokenUriResolver_UnrecognizedCategory();
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Make sure the category is an increment of the previous outfit's category.
|
|
1130
|
+
if (i != 0 && outfitProductCategory <= lastAssetCategory) {
|
|
1131
|
+
revert Banny721TokenUriResolver_UnorderedCategories();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (outfitProductCategory == _HEAD_CATEGORY) {
|
|
1135
|
+
hasHead = true;
|
|
1136
|
+
} else if (outfitProductCategory == _SUIT_CATEGORY) {
|
|
1137
|
+
hasSuit = true;
|
|
1138
|
+
} else if (
|
|
1139
|
+
(outfitProductCategory == _EYES_CATEGORY
|
|
1140
|
+
|| outfitProductCategory == _GLASSES_CATEGORY
|
|
1141
|
+
|| outfitProductCategory == _MOUTH_CATEGORY
|
|
1142
|
+
|| outfitProductCategory == _HEADTOP_CATEGORY) && hasHead
|
|
1143
|
+
) {
|
|
1144
|
+
revert Banny721TokenUriResolver_HeadAlreadyAdded();
|
|
1145
|
+
} else if (
|
|
1146
|
+
(outfitProductCategory == _SUIT_TOP_CATEGORY || outfitProductCategory == _SUIT_BOTTOM_CATEGORY)
|
|
1147
|
+
&& hasSuit
|
|
1148
|
+
) {
|
|
1149
|
+
revert Banny721TokenUriResolver_SuitAlreadyAdded();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Remove all previous assets up to and including the current category being iterated on.
|
|
1153
|
+
// This inner loop advances through `previousOutfitIds` (bounded by outfit category count) and
|
|
1154
|
+
// terminates when it passes the current category or exhausts the array.
|
|
1155
|
+
while (previousOutfitProductCategory <= outfitProductCategory && previousOutfitProductCategory != 0) {
|
|
1156
|
+
// Transfer the previous outfit to the owner of the banny if its not being worn.
|
|
1157
|
+
// `_attachedOutfitIdsOf` hasnt been called yet, so the wearer should still be the banny body being
|
|
1158
|
+
// decorated.
|
|
1159
|
+
if (previousOutfitId != outfitId && wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
|
|
1160
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1161
|
+
_transferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
if (++previousOutfitIndex < previousOutfitIds.length) {
|
|
1165
|
+
// set the next previous outfit.
|
|
1166
|
+
previousOutfitId = previousOutfitIds[previousOutfitIndex];
|
|
1167
|
+
// Get the next previous outfit.
|
|
1168
|
+
previousOutfitProductCategory = _productOfTokenId({hook: hook, tokenId: previousOutfitId}).category;
|
|
1169
|
+
} else {
|
|
1170
|
+
previousOutfitId = 0;
|
|
1171
|
+
previousOutfitProductCategory = 0;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// If the outfit is not already being worn by the banny, transfer it to this contract.
|
|
1176
|
+
if (wearerOf({hook: hook, outfitId: outfitId}) != bannyBodyId) {
|
|
1177
|
+
// Store the banny that's in the background.
|
|
1178
|
+
_wearerOf[hook][outfitId] = bannyBodyId;
|
|
1179
|
+
|
|
1180
|
+
// Transfer the outfit to this contract.
|
|
1181
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1182
|
+
if (owner != address(this)) {
|
|
1183
|
+
_transferFrom({hook: hook, from: _msgSender(), to: address(this), assetId: outfitId});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Keep a reference to the last outfit's category.
|
|
1188
|
+
lastAssetCategory = outfitProductCategory;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Remove and transfer out any remaining assets no longer being worn.
|
|
1192
|
+
// This loop is bounded by `previousOutfitIds.length`, which equals the number of outfits previously
|
|
1193
|
+
// attached to this banny. Since only one outfit per category is allowed, this is bounded by the number of
|
|
1194
|
+
// outfit categories (a small, fixed set).
|
|
1195
|
+
while (previousOutfitId != 0) {
|
|
1196
|
+
// `_attachedOutfitIdsOf` hasnt been called yet, so the wearer should still be the banny body being
|
|
1197
|
+
// decorated.
|
|
1198
|
+
if (wearerOf({hook: hook, outfitId: previousOutfitId}) == bannyBodyId) {
|
|
1199
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1200
|
+
_transferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousOutfitId});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (++previousOutfitIndex < previousOutfitIds.length) {
|
|
1204
|
+
// remove previous product.
|
|
1205
|
+
previousOutfitId = previousOutfitIds[previousOutfitIndex];
|
|
1206
|
+
} else {
|
|
1207
|
+
previousOutfitId = 0;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Store the outfits.
|
|
1212
|
+
_attachedOutfitIdsOf[hook][bannyBodyId] = outfitIds;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/// @notice Add a background to a banny body.
|
|
1216
|
+
/// @param hook The hook storing the assets.
|
|
1217
|
+
/// @param bannyBodyId The ID of the banny body being dressed.
|
|
1218
|
+
/// @param backgroundId The ID of the background that'll be associated with the specified banny.
|
|
1219
|
+
function _decorateBannyWithBackground(address hook, uint256 bannyBodyId, uint256 backgroundId) internal {
|
|
1220
|
+
// Keep a reference to the previous background attached.
|
|
1221
|
+
uint256 previousBackgroundId = _attachedBackgroundIdOf[hook][bannyBodyId];
|
|
1222
|
+
|
|
1223
|
+
// Keep a reference to the user of the previous background.
|
|
1224
|
+
uint256 userOfPreviousBackground = userOf({hook: hook, backgroundId: previousBackgroundId});
|
|
1225
|
+
|
|
1226
|
+
// If the background is changing, add the latest background and transfer the old one back to the owner.
|
|
1227
|
+
if (backgroundId != previousBackgroundId || userOfPreviousBackground != bannyBodyId) {
|
|
1228
|
+
// If there's a previous background worn by this banny, transfer it back to the owner.
|
|
1229
|
+
if (userOfPreviousBackground == bannyBodyId) {
|
|
1230
|
+
// Transfer the previous background to the owner of the banny.
|
|
1231
|
+
_transferFrom({hook: hook, from: address(this), to: _msgSender(), assetId: previousBackgroundId});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Add the background if needed.
|
|
1235
|
+
if (backgroundId != 0) {
|
|
1236
|
+
// Keep a reference to the background's owner.
|
|
1237
|
+
address owner = IERC721(hook).ownerOf(backgroundId);
|
|
1238
|
+
|
|
1239
|
+
// Check if the call is being made by the background's owner, or the owner of a banny body using it.
|
|
1240
|
+
if (_msgSender() != owner) {
|
|
1241
|
+
// Get the banny body currently using this background.
|
|
1242
|
+
uint256 userId = userOf({hook: hook, backgroundId: backgroundId});
|
|
1243
|
+
|
|
1244
|
+
// If the background is not currently used, only the background's owner can use it for decoration.
|
|
1245
|
+
if (userId == 0) revert Banny721TokenUriResolver_UnauthorizedBackground();
|
|
1246
|
+
|
|
1247
|
+
// If the background is used, the banny body's owner can also authorize its use.
|
|
1248
|
+
if (_msgSender() != IERC721(hook).ownerOf(userId)) {
|
|
1249
|
+
revert Banny721TokenUriResolver_UnauthorizedBackground();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Get the background's product info.
|
|
1254
|
+
JB721Tier memory backgroundProduct = _productOfTokenId({hook: hook, tokenId: backgroundId});
|
|
1255
|
+
|
|
1256
|
+
// Background must exist and must be a background category.
|
|
1257
|
+
if (backgroundProduct.id == 0 || backgroundProduct.category != _BACKGROUND_CATEGORY) {
|
|
1258
|
+
revert Banny721TokenUriResolver_UnrecognizedBackground();
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Store the background for the banny.
|
|
1262
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1263
|
+
_attachedBackgroundIdOf[hook][bannyBodyId] = backgroundId;
|
|
1264
|
+
|
|
1265
|
+
// Store the banny that's in the background.
|
|
1266
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1267
|
+
_userOf[hook][backgroundId] = bannyBodyId;
|
|
1268
|
+
|
|
1269
|
+
// Transfer the background to this contract if it's not already owned by this contract.
|
|
1270
|
+
if (owner != address(this)) {
|
|
1271
|
+
_transferFrom({hook: hook, from: _msgSender(), to: address(this), assetId: backgroundId});
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
// slither-disable-next-line reentrancy-no-eth
|
|
1275
|
+
_attachedBackgroundIdOf[hook][bannyBodyId] = 0;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/// @notice Transfer a token from one address to another.
|
|
1281
|
+
/// @param hook The 721 contract of the token being transferred.
|
|
1282
|
+
/// @param from The address to transfer the token from.
|
|
1283
|
+
/// @param to The address to transfer the token to.
|
|
1284
|
+
/// @param assetId The ID of the token to transfer.
|
|
1285
|
+
function _transferFrom(address hook, address from, address to, uint256 assetId) internal {
|
|
1286
|
+
IERC721(hook).safeTransferFrom({from: from, to: to, tokenId: assetId});
|
|
1287
|
+
}
|
|
1288
|
+
}
|