@paulstinchcombe/gasless-nft-tx 0.11.3 → 0.12.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.
Files changed (20) hide show
  1. package/dist/KAMI-NFTs/KAMI1155CUpgradeable.sol +1069 -0
  2. package/dist/KAMI-NFTs/KAMI721ACUpgradable.sol +949 -0
  3. package/dist/KAMI-NFTs/KAMI721CUpgradeable.sol +691 -0
  4. package/dist/KAMI-NFTs/ProxyAdmin.sol +12 -0
  5. package/dist/KAMI-NFTs/TransparentUpgradeableProxy.sol +15 -0
  6. package/dist/KAMI-NFTs/artifacts/contracts/KAMI1155CUpgradeable.sol/KAMI1155CUpgradeable.dbg.json +4 -0
  7. package/dist/KAMI-NFTs/artifacts/contracts/KAMI1155CUpgradeable.sol/KAMI1155CUpgradeable.json +2207 -0
  8. package/dist/KAMI-NFTs/artifacts/contracts/KAMI721ACUpgradable.sol/KAMI721ACUpgradable.dbg.json +4 -0
  9. package/dist/KAMI-NFTs/artifacts/contracts/KAMI721ACUpgradable.sol/KAMI721ACUpgradable.json +2210 -0
  10. package/dist/KAMI-NFTs/artifacts/contracts/KAMI721CUpgradeable.sol/KAMI721CUpgradeable.dbg.json +4 -0
  11. package/dist/KAMI-NFTs/artifacts/contracts/KAMI721CUpgradeable.sol/KAMI721CUpgradeable.json +1823 -0
  12. package/dist/KAMI-NFTs/artifacts/contracts/ProxyAdmin.sol/KAMIProxyAdmin.dbg.json +4 -0
  13. package/dist/KAMI-NFTs/artifacts/contracts/ProxyAdmin.sol/KAMIProxyAdmin.json +132 -0
  14. package/dist/KAMI-NFTs/artifacts/contracts/TransparentUpgradeableProxy.sol/KAMITransparentUpgradeableProxy.dbg.json +4 -0
  15. package/dist/KAMI-NFTs/artifacts/contracts/TransparentUpgradeableProxy.sol/KAMITransparentUpgradeableProxy.json +116 -0
  16. package/dist/kami-upgrade-manager.d.ts +65 -0
  17. package/dist/kami-upgrade-manager.d.ts.map +1 -0
  18. package/dist/kami-upgrade-manager.js +320 -0
  19. package/dist/kami-upgrade-manager.js.map +1 -0
  20. package/package.json +1 -1
@@ -0,0 +1,949 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import {ERC2981Upgradeable} from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol";
5
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
9
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
10
+ import {ERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
11
+ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
12
+ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
13
+ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
14
+ import {KamiNFTCore} from "./libraries/KamiNFTCore.sol";
15
+ import {KamiPlatform} from "./libraries/KamiPlatform.sol";
16
+ import {KamiRoyalty} from "./libraries/KamiRoyalty.sol";
17
+ import {KamiRental} from "./libraries/KamiRental.sol";
18
+ import {KamiTransfer} from "./libraries/KamiTransfer.sol";
19
+
20
+ /**
21
+ * @title KAMI721ACUpgradable
22
+ * @dev Upgradeable claimable ERC721 contract with advanced royalty, platform commission, and rental features.
23
+ * - One claim per address (no traditional minting)
24
+ * - ERC20 payment integration
25
+ * - Platform commission system
26
+ * - Time-based rental system
27
+ * - Royalty enforcement
28
+ * - Role-based access control
29
+ * - Batch claiming
30
+ * - UUPS upgradeable pattern
31
+ *
32
+ * Uses split libraries for size optimization and SONEUM deployment compatibility.
33
+ * @author Paul Stinchcombe
34
+ * @custom:security-contact security@kami.com
35
+ */
36
+ contract KAMI721ACUpgradable is
37
+ Initializable,
38
+ AccessControlUpgradeable,
39
+ ERC721EnumerableUpgradeable,
40
+ ERC2981Upgradeable,
41
+ PausableUpgradeable,
42
+ UUPSUpgradeable
43
+ {
44
+ using SafeERC20 for IERC20;
45
+ // Libraries called explicitly to reduce contract size
46
+
47
+ // ============ CUSTOM ERRORS ============
48
+ error InvalidPaymentTokenAddress();
49
+ error InvalidPlatformAddress();
50
+ error PlatformCommissionTooHigh();
51
+ error CallerNotOwner();
52
+ error EmptyRecipientsArray();
53
+ error TooManyRecipients();
54
+ error InvalidRecipientAddress();
55
+ error RecipientAlreadyClaimed();
56
+ error AlreadyClaimed();
57
+ error TokenPriceNotSet();
58
+ error SalePriceNotSet();
59
+ error MintPriceNotSet();
60
+ error InvalidPrice();
61
+ error EmptyTokenURI();
62
+ error SellerNotTokenOwner();
63
+ error OwnerCannotRentOwnToken();
64
+ error ZeroAddress();
65
+ error QueryForNonexistentToken();
66
+ error CallerNotTokenOwnerOrApproved();
67
+ error ArrayLengthMismatch();
68
+ error TokenSupplyExceeded();
69
+
70
+ // ============ STORAGE VARIABLES ============
71
+
72
+ /** @dev Transfer tracker for royalty enforcement - managed by library */
73
+ KamiNFTCore.TransferTracker private _transferTracker;
74
+
75
+ /** @dev Role definitions from library */
76
+ bytes32 public constant OWNER_ROLE = KamiNFTCore.OWNER_ROLE;
77
+ bytes32 public constant RENTER_ROLE = KamiNFTCore.RENTER_ROLE;
78
+ bytes32 public constant PLATFORM_ROLE = KamiNFTCore.PLATFORM_ROLE;
79
+
80
+ /** @dev Token ID counter for sequential token generation */
81
+ uint256 private _tokenIdCounter;
82
+
83
+ /** @dev Global mint/claim price (settable by OWNER_ROLE) */
84
+ uint256 public mintPrice;
85
+
86
+ /** @dev Per-token sale prices (settable by token owner) */
87
+ mapping(uint256 => uint256) public salePrices;
88
+
89
+ /** @dev Backward compatibility: tokenPrices returns salePrices */
90
+ mapping(uint256 => uint256) public tokenPrices;
91
+
92
+ /** @dev Individual URI for each token */
93
+ mapping(uint256 => string) public tokenURIs;
94
+
95
+ /** @dev Base URI for token metadata (fallback) */
96
+ string private _baseTokenURI;
97
+
98
+ /** @dev ERC20 token used for all payments */
99
+ IERC20 public paymentToken;
100
+
101
+ /** @dev Mapping to track which addresses have already claimed a token */
102
+ mapping(address => bool) public hasClaimed;
103
+
104
+ /** @dev Maximum total supply for the contract (0 means unlimited) */
105
+ uint256 private _maxTotalSupply;
106
+
107
+ /// @dev Storage gap for upgradeable contracts
108
+ /// Reserved slots:
109
+ /// - Slots 0-39: Reserved for base contract upgrades
110
+ /// - Slots 40-49: Reserved for future multiple payment token feature (10 slots)
111
+ /// Future storage (commented out, to be uncommented in future upgrade):
112
+ /// - mapping(uint256 => address[]) public tokenPaymentTokens; // Supported payment tokens per token
113
+ /// - mapping(uint256 => mapping(address => uint256)) public tokenPricesByPaymentToken; // Price per token per payment token
114
+ /// - address[] public activePaymentTokens; // Global list of accepted payment tokens
115
+ /// Note: Mappings don't consume storage slots, but we reserve slots for array lengths and configuration flags
116
+ uint256[40] private __gap;
117
+
118
+ // ============ EVENTS ============
119
+
120
+ /**
121
+ * @dev Emitted when a token is successfully claimed
122
+ * @param claimer Address that claimed the token
123
+ * @param tokenId ID of the claimed token
124
+ * @param paymentAmount Amount paid for the claim
125
+ */
126
+ event TokenClaimed(address indexed claimer, uint256 indexed tokenId, uint256 paymentAmount);
127
+
128
+ /**
129
+ * @dev Emitted when tokens are batch claimed (owner pays for all)
130
+ * @param owner Address that paid for all tokens
131
+ * @param recipients Array of addresses that received tokens
132
+ * @param totalPayment Total amount paid by owner
133
+ */
134
+ event BatchClaimedFor(address indexed owner, address[] recipients, uint256 totalPayment);
135
+
136
+ /**
137
+ * @dev Emitted when tokens are batch claimed (each recipient pays)
138
+ * @param caller Address that initiated the batch claim
139
+ * @param recipients Array of addresses that received tokens
140
+ */
141
+ event BatchClaimed(address indexed caller, address[] recipients);
142
+
143
+ /**
144
+ * @dev Emitted when the global mint price is updated
145
+ * @param oldPrice Previous mint price
146
+ * @param newPrice New mint price
147
+ */
148
+ event MintPriceUpdated(uint256 oldPrice, uint256 newPrice);
149
+
150
+ /**
151
+ * @dev Emitted when a token's sale price is updated
152
+ * @param tokenId Token ID
153
+ * @param oldPrice Previous sale price
154
+ * @param newPrice New sale price
155
+ */
156
+ event SalePriceUpdated(uint256 indexed tokenId, uint256 oldPrice, uint256 newPrice);
157
+
158
+ // ============ INITIALIZER ============
159
+
160
+ /**
161
+ * @notice Initializes the contract with configuration.
162
+ * @param paymentToken_ ERC20 token address for payments
163
+ * @param name_ NFT collection name
164
+ * @param symbol_ NFT collection symbol
165
+ * @param baseTokenURI_ Base URI for token metadata
166
+ * @param platformAddress_ Platform commission recipient
167
+ * @param platformCommissionPercentage_ Platform commission (basis points, max 2000 = 20%)
168
+ * @param adminAddress_ Address to receive admin and owner roles
169
+ * @param totalSupply_ Optional total supply limit (0 means unlimited for this contract)
170
+ * @param mintPrice_ Optional initial mint price (default 0)
171
+ */
172
+ function initialize(
173
+ address paymentToken_,
174
+ string memory name_,
175
+ string memory symbol_,
176
+ string memory baseTokenURI_,
177
+ address platformAddress_,
178
+ uint96 platformCommissionPercentage_,
179
+ address adminAddress_,
180
+ uint256 totalSupply_,
181
+ uint256 mintPrice_
182
+ ) public initializer {
183
+ if (paymentToken_ == address(0)) revert InvalidPaymentTokenAddress();
184
+ if (platformAddress_ == address(0)) revert InvalidPlatformAddress();
185
+ if (adminAddress_ == address(0)) revert ZeroAddress();
186
+ if (platformCommissionPercentage_ > 2000) revert PlatformCommissionTooHigh();
187
+
188
+ __ERC721_init(name_, symbol_);
189
+ __ERC721Enumerable_init();
190
+ __ERC2981_init();
191
+ __AccessControl_init();
192
+ __Pausable_init();
193
+ __UUPSUpgradeable_init();
194
+
195
+ paymentToken = IERC20(paymentToken_);
196
+ _baseTokenURI = baseTokenURI_;
197
+
198
+ // Initialize libraries
199
+ KamiPlatform.initializePlatform(platformAddress_, platformCommissionPercentage_);
200
+ KamiRoyalty.initializeRoyaltyConfig();
201
+
202
+ // Set up roles - use adminAddress_ instead of msg.sender
203
+ _grantRole(DEFAULT_ADMIN_ROLE, adminAddress_);
204
+ _grantRole(OWNER_ROLE, adminAddress_);
205
+ _grantRole(PLATFORM_ROLE, platformAddress_);
206
+
207
+ // Initialize token ID counter
208
+ _tokenIdCounter = 1;
209
+
210
+ // Set total supply if provided (0 means unlimited)
211
+ if (totalSupply_ > 0) {
212
+ _maxTotalSupply = totalSupply_;
213
+ }
214
+
215
+ // Set initial mint price
216
+ mintPrice = mintPrice_;
217
+ }
218
+
219
+ /**
220
+ * @notice Checks interface support (ERC165).
221
+ * @param interfaceId Interface identifier
222
+ * @return True if supported
223
+ */
224
+ function supportsInterface(bytes4 interfaceId)
225
+ public
226
+ view
227
+ virtual
228
+ override(ERC721EnumerableUpgradeable, ERC2981Upgradeable, AccessControlUpgradeable)
229
+ returns (bool)
230
+ {
231
+ return super.supportsInterface(interfaceId);
232
+ }
233
+
234
+ // ============ PLATFORM COMMISSION MANAGEMENT ============
235
+
236
+ /**
237
+ * @notice Update platform commission settings (OWNER_ROLE only).
238
+ * @param newPlatformCommissionPercentage New commission (basis points, max 2000)
239
+ * @param newPlatformAddress New platform address
240
+ */
241
+ function setPlatformCommission(uint96 newPlatformCommissionPercentage, address newPlatformAddress) external {
242
+ address oldPlatformAddress = KamiPlatform.platformAddress();
243
+ KamiPlatform.updatePlatformCommission(newPlatformCommissionPercentage, newPlatformAddress, address(this));
244
+ if (oldPlatformAddress != newPlatformAddress) {
245
+ if (hasRole(PLATFORM_ROLE, oldPlatformAddress)) {
246
+ _revokeRole(PLATFORM_ROLE, oldPlatformAddress);
247
+ }
248
+ _grantRole(PLATFORM_ROLE, newPlatformAddress);
249
+ }
250
+ }
251
+
252
+ // ============ ROYALTY MANAGEMENT ============
253
+
254
+ /**
255
+ * @notice Set global royalty percentage (OWNER_ROLE only).
256
+ * @param newRoyaltyPercentage New royalty (basis points, max 10000)
257
+ */
258
+ function setRoyaltyPercentage(uint96 newRoyaltyPercentage) external {
259
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
260
+ KamiRoyalty.setRoyaltyPercentage(newRoyaltyPercentage);
261
+ }
262
+
263
+ /**
264
+ * @notice Set mint royalty receivers (OWNER_ROLE only).
265
+ * @param royalties Array of RoyaltyData (receiver, feeNumerator)
266
+ */
267
+ function setMintRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external {
268
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
269
+ KamiRoyalty.setMintRoyalties(royalties);
270
+ }
271
+
272
+ /**
273
+ * @notice Set transfer royalty receivers (OWNER_ROLE only).
274
+ * @param royalties Array of RoyaltyData (receiver, feeNumerator)
275
+ */
276
+ function setTransferRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external {
277
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
278
+ KamiRoyalty.setTransferRoyalties(royalties);
279
+ }
280
+
281
+ /**
282
+ * @notice Set token-specific mint royalties (OWNER_ROLE only).
283
+ * @param tokenId Token ID
284
+ * @param royalties Array of RoyaltyData
285
+ */
286
+ function setTokenMintRoyalties(uint256 tokenId, KamiNFTCore.RoyaltyData[] calldata royalties) external {
287
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
288
+ KamiRoyalty.setTokenMintRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
289
+ }
290
+
291
+ /**
292
+ * @notice Set token-specific transfer royalties (OWNER_ROLE only).
293
+ * @param tokenId Token ID
294
+ * @param royalties Array of RoyaltyData
295
+ */
296
+ function setTokenTransferRoyalties(uint256 tokenId, KamiNFTCore.RoyaltyData[] calldata royalties) external {
297
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
298
+ KamiRoyalty.setTokenTransferRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
299
+ }
300
+
301
+ /**
302
+ * @notice Royalty info for a token sale (ERC2981).
303
+ * @dev Uses salePrice parameter if provided, otherwise falls back to salePrices mapping
304
+ * @param tokenId Token ID
305
+ * @param salePrice Sale price (if 0, uses salePrices[tokenId])
306
+ * @return receiver Royalty receiver (first receiver for ERC2981 compatibility)
307
+ * @return royaltyAmount Royalty amount (first receiver's share)
308
+ */
309
+ function royaltyInfo(uint256 tokenId, uint256 salePrice)
310
+ public
311
+ view
312
+ override
313
+ returns (address receiver, uint256 royaltyAmount)
314
+ {
315
+ uint256 price = salePrice > 0 ? salePrice : salePrices[tokenId];
316
+ if (price == 0) {
317
+ return (address(0), 0);
318
+ }
319
+
320
+ uint256 totalRoyaltyAmount = (price * KamiRoyalty.royaltyPercentage()) / 10000;
321
+ KamiNFTCore.RoyaltyData[] memory royalties = KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
322
+ if (royalties.length > 0) {
323
+ KamiNFTCore.RoyaltyData memory info = royalties[0];
324
+ uint256 receiverShare = (totalRoyaltyAmount * info.feeNumerator) / 10000;
325
+ return (info.receiver, receiverShare);
326
+ }
327
+ return (address(0), 0);
328
+ }
329
+
330
+ /**
331
+ * @notice Get all royalty receivers and their amounts for a token sale.
332
+ * @param tokenId Token ID
333
+ * @return receivers Array of royalty receiver addresses
334
+ * @return amounts Array of royalty amounts for each receiver
335
+ */
336
+ function getRoyaltyInfo(uint256 tokenId)
337
+ public
338
+ view
339
+ returns (address[] memory receivers, uint256[] memory amounts)
340
+ {
341
+ uint256 price = salePrices[tokenId];
342
+ if (price == 0) {
343
+ return (new address[](0), new uint256[](0));
344
+ }
345
+
346
+ uint256 totalRoyaltyAmount = (price * KamiRoyalty.royaltyPercentage()) / 10000;
347
+ KamiNFTCore.RoyaltyData[] memory royalties = KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
348
+
349
+ if (royalties.length == 0) {
350
+ return (new address[](0), new uint256[](0));
351
+ }
352
+
353
+ receivers = new address[](royalties.length);
354
+ amounts = new uint256[](royalties.length);
355
+
356
+ for (uint256 i = 0; i < royalties.length; i++) {
357
+ receivers[i] = royalties[i].receiver;
358
+ amounts[i] = (totalRoyaltyAmount * royalties[i].feeNumerator) / 10000;
359
+ }
360
+
361
+ return (receivers, amounts);
362
+ }
363
+
364
+ /**
365
+ * @notice Get mint royalty receivers for a token.
366
+ * @param tokenId Token ID
367
+ * @return Array of RoyaltyData
368
+ */
369
+ function getMintRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
370
+ return KamiRoyalty.getMintRoyaltyReceivers(tokenId);
371
+ }
372
+
373
+ /**
374
+ * @notice Get transfer royalty receivers for a token.
375
+ * @param tokenId Token ID
376
+ * @return Array of RoyaltyData
377
+ */
378
+ function getTransferRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
379
+ return KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
380
+ }
381
+
382
+ function exists(uint256 tokenId) public view returns (bool) {
383
+ try this.ownerOf(tokenId) returns (address) {
384
+ return true;
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * @dev Returns an internal view function to check if an operator is approved for all tokens.
392
+ * This is used for compatibility with library functions that require an internal function pointer.
393
+ */
394
+ function _getIsApprovedForAllReference() internal pure returns (function(address, address) view returns (bool)) {
395
+ return isApprovedForAll;
396
+ }
397
+
398
+ // ============ TOKEN SALES ============
399
+
400
+ /**
401
+ * @notice Sell a token with royalty distribution.
402
+ * @param to Buyer address
403
+ * @param tokenId Token ID
404
+ * @param seller Seller address
405
+ */
406
+ function sellToken(address to, uint256 tokenId, address seller) external {
407
+ if (seller == address(0)) revert ZeroAddress();
408
+ if (ownerOf(tokenId) != seller) revert SellerNotTokenOwner();
409
+ uint256 price = salePrices[tokenId];
410
+ if (price == 0) revert SalePriceNotSet();
411
+
412
+ // Process sale with royalties FIRST (marks transfer as paid)
413
+ KamiTransfer.sellToken(IERC20(address(paymentToken)), tokenId, to, price, seller);
414
+
415
+ // Then transfer token (will now pass validation)
416
+ safeTransferFrom(seller, to, tokenId);
417
+ }
418
+
419
+ // ============ RENTAL SYSTEM ============
420
+
421
+ /**
422
+ * @notice Rent a token for a duration.
423
+ * @param tokenId Token ID
424
+ * @param duration Rental duration (seconds)
425
+ * @param rentalPrice Total rental price
426
+ */
427
+ function rentToken(uint256 tokenId, uint256 duration, uint256 rentalPrice, address renter) external whenNotPaused {
428
+ if (renter == address(0)) revert ZeroAddress();
429
+ address tokenOwner = ownerOf(tokenId);
430
+ if (tokenOwner == renter) revert OwnerCannotRentOwnToken();
431
+ KamiRental.rentToken(IERC20(address(paymentToken)), tokenId, duration, rentalPrice, tokenOwner, msg.sender);
432
+ _grantRole(RENTER_ROLE, renter);
433
+ }
434
+
435
+ /**
436
+ * @notice End a rental early.
437
+ * @param tokenId Token ID
438
+ */
439
+ function endRental(uint256 tokenId) external whenNotPaused {
440
+ // address tokenOwner = ownerOf(tokenId); // Unused variable
441
+ KamiRental.endRentalSimple(tokenId);
442
+ // mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals(); // Unused variable
443
+ address renter = KamiRental.getRentalInfo(tokenId).renter;
444
+ if (!hasActiveRentals(renter)) {
445
+ _revokeRole(RENTER_ROLE, renter);
446
+ }
447
+ }
448
+
449
+ /**
450
+ * @notice Extend a rental period.
451
+ * @param tokenId Token ID
452
+ * @param additionalDuration Additional duration (seconds)
453
+ * @param additionalPayment Additional payment
454
+ */
455
+ function extendRental(uint256 tokenId, uint256 additionalDuration, uint256 additionalPayment) external whenNotPaused {
456
+ address tokenOwner = ownerOf(tokenId);
457
+ KamiRental.extendRental(IERC20(address(paymentToken)), tokenId, additionalDuration, additionalPayment, tokenOwner, msg.sender);
458
+ }
459
+
460
+ /**
461
+ * @notice Check if a token is currently rented.
462
+ * @param tokenId Token ID
463
+ * @return True if rented
464
+ */
465
+ function isRented(uint256 tokenId) external view whenNotPaused returns (bool) {
466
+ return KamiRental.isRented(tokenId);
467
+ }
468
+
469
+ /**
470
+ * @notice Get rental info for a token.
471
+ * @param tokenId Token ID
472
+ * @return renter Renter address
473
+ * @return startTime Rental start
474
+ * @return endTime Rental end
475
+ * @return rentalPrice Rental price
476
+ * @return active Rental active
477
+ */
478
+ function getRentalInfo(uint256 tokenId) external view whenNotPaused returns (
479
+ address renter,
480
+ uint256 startTime,
481
+ uint256 endTime,
482
+ uint256 rentalPrice,
483
+ bool active
484
+ ) {
485
+ KamiNFTCore.Rental memory rental = KamiRental.getRentalInfo(tokenId);
486
+ return (rental.renter, rental.startTime, rental.endTime, rental.rentalPrice, rental.active);
487
+ }
488
+
489
+ /**
490
+ * @notice Check if a user has active rentals.
491
+ * @param user User address
492
+ * @return True if user has active rentals
493
+ */
494
+ function hasActiveRentals(address user) public view whenNotPaused returns (bool) {
495
+ mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals();
496
+ uint256 supply = _tokenIdCounter;
497
+ for (uint256 i = 0; i < supply; i++) {
498
+ uint256 tokenId = tokenByIndex(i);
499
+ if (rentals[tokenId].active && rentals[tokenId].renter == user) {
500
+ return true;
501
+ }
502
+ }
503
+ return false;
504
+ }
505
+
506
+ // ============ TOKEN TRANSFER VALIDATION ============
507
+
508
+ /**
509
+ * @dev Hook before any token transfer. Handles rental expiry, role management, and transfer validation.
510
+ */
511
+ function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) {
512
+ address from = _ownerOf(tokenId);
513
+ address owner = super._update(to, tokenId, auth);
514
+
515
+ // Skip validation for minting (from == owner means minting)
516
+ if (from != owner && from != address(0)) {
517
+ // Check for expired rentals
518
+ if (KamiRental.isRented(tokenId)) {
519
+ KamiNFTCore.Rental memory rental = KamiRental.getRentalInfo(tokenId);
520
+ if (block.timestamp >= rental.endTime) {
521
+ address rentalRenter = rental.renter;
522
+ if (!hasActiveRentals(rentalRenter)) {
523
+ _revokeRole(RENTER_ROLE, rentalRenter);
524
+ }
525
+ }
526
+ }
527
+
528
+ // Validate transfer
529
+ KamiTransfer.validateTransfer(
530
+ tokenId,
531
+ from,
532
+ to,
533
+ isApprovedForAll
534
+ );
535
+
536
+ // Update rental status on transfer
537
+ KamiTransfer.updateRentalOnTransferSimple(tokenId);
538
+ }
539
+
540
+ return owner;
541
+ }
542
+
543
+ // ============ METADATA MANAGEMENT ============
544
+
545
+ /**
546
+ * @notice Set base URI for token metadata (OWNER_ROLE only).
547
+ * @param baseURI New base URI
548
+ */
549
+ function setBaseURI(string memory baseURI) external {
550
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
551
+ _baseTokenURI = baseURI;
552
+ }
553
+
554
+ /**
555
+ * @notice Get base URI for token metadata.
556
+ * @return Base URI string
557
+ */
558
+ function _baseURI() internal view virtual override returns (string memory) {
559
+ return _baseTokenURI;
560
+ }
561
+
562
+ // ============ TOKEN BURNING ============
563
+
564
+ /**
565
+ * @notice Burn a token (token owner only).
566
+ * @param tokenId Token ID
567
+ */
568
+ function burn(uint256 tokenId) external {
569
+ KamiTransfer.validateBurn(tokenId, ownerOf(tokenId));
570
+ _burn(tokenId);
571
+ }
572
+
573
+ // ============ PAUSE FUNCTIONALITY ============
574
+
575
+ /**
576
+ * @notice Pause all non-view functions (OWNER_ROLE only).
577
+ */
578
+ function pause() external {
579
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
580
+ _pause();
581
+ }
582
+
583
+ /**
584
+ * @notice Unpause all functions (OWNER_ROLE only).
585
+ */
586
+ function unpause() external {
587
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
588
+ _unpause();
589
+ }
590
+
591
+ // ============ ROYALTY TRANSFER FUNCTIONS ============
592
+
593
+ /**
594
+ * @notice Initiate a transfer with royalty requirement.
595
+ * @param to Recipient address
596
+ * @param tokenId Token ID
597
+ * @param salePrice Sale price
598
+ */
599
+ function initiateTransferWithRoyalty(address to, uint256 tokenId, uint256 salePrice) external {
600
+ KamiTransfer.initiateTransferWithRoyalty(tokenId, to, salePrice, ownerOf(tokenId));
601
+ }
602
+
603
+ /**
604
+ * @notice Pay transfer royalty and complete transfer.
605
+ * @param tokenId Token ID
606
+ * @param salePrice Sale price
607
+ */
608
+ function payTransferRoyalty(address /* to */, uint256 tokenId, uint256 salePrice) external {
609
+ KamiTransfer.payTransferRoyalty(IERC20(address(paymentToken)), tokenId, salePrice, ownerOf(tokenId), msg.sender);
610
+ }
611
+
612
+ /**
613
+ * @notice Check if transfer royalty is required.
614
+ * @param tokenId Token ID
615
+ * @param salePrice Sale price
616
+ * @return True if royalty required
617
+ */
618
+ function isTransferRoyaltyRequired(address /* from */, address /* to */, uint256 tokenId, uint256 salePrice) external view returns (bool) {
619
+ return KamiTransfer.isTransferRoyaltyRequired(tokenId, salePrice);
620
+ }
621
+
622
+ // ============ GETTER FUNCTIONS ============
623
+
624
+ /**
625
+ * @notice Get platform commission percentage.
626
+ * @return Commission (basis points)
627
+ */
628
+ function platformCommissionPercentage() public view returns (uint96) {
629
+ return KamiPlatform.platformCommission();
630
+ }
631
+
632
+ /**
633
+ * @notice Get platform address.
634
+ * @return Platform address
635
+ */
636
+ function platformAddress() public view returns (address) {
637
+ return KamiPlatform.platformAddress();
638
+ }
639
+
640
+ /**
641
+ * @notice Get global royalty percentage.
642
+ * @return Royalty (basis points)
643
+ */
644
+ function royaltyPercentage() public view returns (uint96) {
645
+ return KamiRoyalty.royaltyPercentage();
646
+ }
647
+
648
+ /**
649
+ * @notice Set global mint price (OWNER_ROLE only).
650
+ * @param newMintPrice New mint price in payment token
651
+ */
652
+ function setMintPrice(uint256 newMintPrice) external {
653
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
654
+ uint256 oldPrice = mintPrice;
655
+ mintPrice = newMintPrice;
656
+ emit MintPriceUpdated(oldPrice, newMintPrice);
657
+ }
658
+
659
+ /**
660
+ * @notice Set sale price for a specific token (token owner only).
661
+ * @param tokenId Token ID
662
+ * @param newSalePrice New sale price in payment token
663
+ */
664
+ function setSalePrice(uint256 tokenId, uint256 newSalePrice) external {
665
+ if (_ownerOf(tokenId) != msg.sender) revert CallerNotTokenOwnerOrApproved();
666
+ if (_ownerOf(tokenId) == address(0)) revert KamiRoyalty.TokenDoesNotExist();
667
+ uint256 oldPrice = salePrices[tokenId];
668
+ salePrices[tokenId] = newSalePrice;
669
+ tokenPrices[tokenId] = newSalePrice; // Backward compatibility
670
+ emit SalePriceUpdated(tokenId, oldPrice, newSalePrice);
671
+ }
672
+
673
+ /**
674
+ * @notice Set price for a specific token (OWNER_ROLE only) - DEPRECATED, use setSalePrice
675
+ * @dev Kept for backward compatibility, but delegates to setSalePrice
676
+ * @param tokenId Token ID
677
+ * @param newPrice New price in payment token
678
+ */
679
+ function setPrice(uint256 tokenId, uint256 newPrice) external {
680
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
681
+ if (_ownerOf(tokenId) == address(0)) revert KamiRoyalty.TokenDoesNotExist();
682
+ uint256 oldPrice = salePrices[tokenId];
683
+ salePrices[tokenId] = newPrice;
684
+ tokenPrices[tokenId] = newPrice; // Backward compatibility
685
+ emit SalePriceUpdated(tokenId, oldPrice, newPrice);
686
+ }
687
+
688
+ /**
689
+ * @dev Sets the URI for a specific token
690
+ * @param tokenId The token ID
691
+ * @param newTokenURI The new URI for the token's metadata
692
+ *
693
+ * Requirements:
694
+ * - Caller must have OWNER_ROLE
695
+ * - Token must exist
696
+ * - Token URI cannot be empty
697
+ */
698
+ function setTokenURI(uint256 tokenId, string calldata newTokenURI) external {
699
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
700
+ if (_ownerOf(tokenId) == address(0)) revert KamiRoyalty.TokenDoesNotExist();
701
+ if (bytes(newTokenURI).length == 0) revert EmptyTokenURI();
702
+ tokenURIs[tokenId] = newTokenURI;
703
+ }
704
+
705
+ /**
706
+ * @dev Returns the URI for a given token ID
707
+ * @dev Returns individual token URI if set, otherwise falls back to base URI
708
+ * @param tokenId The token ID to query
709
+ * @return The token URI
710
+ */
711
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
712
+ if (_ownerOf(tokenId) == address(0)) revert QueryForNonexistentToken();
713
+
714
+ string memory individualURI = tokenURIs[tokenId];
715
+ if (bytes(individualURI).length > 0) {
716
+ return individualURI;
717
+ }
718
+
719
+ return string(abi.encodePacked(_baseTokenURI, Strings.toString(tokenId)));
720
+ }
721
+
722
+ /**
723
+ * @notice Set total supply limit for the contract (OWNER_ROLE only).
724
+ * @param maxSupply Maximum number of tokens that can be minted/claimed for the contract
725
+ */
726
+ function setTotalSupply(uint256 maxSupply) external {
727
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
728
+ _maxTotalSupply = maxSupply;
729
+ }
730
+
731
+ /**
732
+ * @notice Get total supply limit for the contract.
733
+ * @return Total supply limit (0 means unlimited)
734
+ */
735
+ function maxTotalSupply() public view returns (uint256) {
736
+ return _maxTotalSupply;
737
+ }
738
+
739
+ /**
740
+ * @notice Get the number of tokens minted so far.
741
+ * @return Number of tokens minted
742
+ */
743
+ function getTotalMinted() public view returns (uint256) {
744
+ return totalSupply(); // Use ERC721Enumerable's totalSupply() to get current count
745
+ }
746
+
747
+ // ============ CLAIMING FUNCTIONS ============
748
+
749
+ /**
750
+ * @notice Claim a single token (one per address).
751
+ * Requirements:
752
+ * - Caller must not have already claimed
753
+ * - Caller must have approved sufficient payment tokens
754
+ * - Caller must have sufficient payment token balance
755
+ * - Global mint price must be set
756
+ */
757
+ function claim(string calldata uri, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external {
758
+ if (hasClaimed[msg.sender]) revert AlreadyClaimed();
759
+ if (mintPrice == 0) revert MintPriceNotSet();
760
+ if (bytes(uri).length == 0) revert EmptyTokenURI();
761
+
762
+ // Check total supply limit if set
763
+ if (_maxTotalSupply > 0) {
764
+ uint256 currentSupply = totalSupply(); // Get current count from ERC721Enumerable
765
+ if (currentSupply >= _maxTotalSupply) {
766
+ revert TokenSupplyExceeded();
767
+ }
768
+ }
769
+
770
+ uint256 tokenId = _tokenIdCounter;
771
+ _tokenIdCounter++;
772
+ _safeMint(msg.sender, tokenId);
773
+
774
+ salePrices[tokenId] = mintPrice;
775
+ tokenPrices[tokenId] = mintPrice; // Backward compatibility
776
+ tokenURIs[tokenId] = uri;
777
+
778
+ // Transfer payment to contract
779
+ paymentToken.safeTransferFrom(msg.sender, address(this), mintPrice);
780
+
781
+ // Calculate and deduct platform commission
782
+ uint96 platformCommission = KamiPlatform.platformCommission();
783
+ uint256 commissionAmount = 0;
784
+ if (platformCommission > 0) {
785
+ commissionAmount = (mintPrice * platformCommission) / 10000;
786
+ if (commissionAmount > 0) {
787
+ paymentToken.safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
788
+ }
789
+ }
790
+
791
+ // Calculate remaining amount after platform commission
792
+ uint256 remainingAmount = mintPrice - commissionAmount;
793
+
794
+ // Set token-specific mint royalties if provided
795
+ if(mintRoyalties.length > 0) {
796
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
797
+ }
798
+
799
+ // Distribute mint royalties on the remaining amount
800
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
801
+
802
+ hasClaimed[msg.sender] = true;
803
+ emit TokenClaimed(msg.sender, tokenId, mintPrice);
804
+ }
805
+
806
+ /**
807
+ * @notice Batch claim tokens for recipients (owner pays for all).
808
+ * @param recipients Array of recipient addresses
809
+ * @param uris Array of URIs for each token
810
+ * @param mintRoyalties Array of royalty receivers and percentages
811
+ * Requirements:
812
+ * - Caller must have OWNER_ROLE
813
+ * - All recipients must not have already claimed
814
+ * - Owner must have approved sufficient payment tokens
815
+ * - Recipients array must not be empty or exceed 100
816
+ * - No zero addresses
817
+ * - Global mint price must be set
818
+ */
819
+ function batchClaimFor(address[] calldata recipients, string[] calldata uris, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external {
820
+ if (!hasRole(OWNER_ROLE, msg.sender)) revert CallerNotOwner();
821
+ if (recipients.length == 0) revert EmptyRecipientsArray();
822
+ if (recipients.length > 100) revert TooManyRecipients();
823
+ if (recipients.length != uris.length) revert ArrayLengthMismatch();
824
+ if (mintPrice == 0) revert MintPriceNotSet();
825
+
826
+ uint256 totalCost = mintPrice * recipients.length;
827
+ for (uint256 i = 0; i < uris.length; i++) {
828
+ if (bytes(uris[i]).length == 0) revert EmptyTokenURI();
829
+ }
830
+ paymentToken.safeTransferFrom(msg.sender, address(this), totalCost);
831
+
832
+ for (uint256 i = 0; i < recipients.length; i++) {
833
+ address recipient = recipients[i];
834
+ if (recipient == address(0)) revert InvalidRecipientAddress();
835
+ if (hasClaimed[recipient]) revert RecipientAlreadyClaimed();
836
+
837
+ // Check total supply limit if set (check before minting each token)
838
+ if (_maxTotalSupply > 0) {
839
+ uint256 currentSupply = totalSupply(); // Get current count from ERC721Enumerable
840
+ if (currentSupply >= _maxTotalSupply) {
841
+ revert TokenSupplyExceeded();
842
+ }
843
+ }
844
+
845
+ uint256 tokenId = _tokenIdCounter;
846
+ _tokenIdCounter++;
847
+ _safeMint(recipient, tokenId);
848
+
849
+ salePrices[tokenId] = mintPrice;
850
+ tokenPrices[tokenId] = mintPrice; // Backward compatibility
851
+ tokenURIs[tokenId] = uris[i];
852
+
853
+ // Calculate and deduct platform commission
854
+ uint96 platformCommission = KamiPlatform.platformCommission();
855
+ uint256 commissionAmount = 0;
856
+ if (platformCommission > 0) {
857
+ commissionAmount = (mintPrice * platformCommission) / 10000;
858
+ if (commissionAmount > 0) {
859
+ paymentToken.safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
860
+ }
861
+ }
862
+
863
+ // Calculate remaining amount after platform commission
864
+ uint256 remainingAmount = mintPrice - commissionAmount;
865
+
866
+ if(mintRoyalties.length > 0) {
867
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
868
+ }
869
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
870
+
871
+ hasClaimed[recipient] = true;
872
+ }
873
+ emit BatchClaimedFor(msg.sender, recipients, totalCost);
874
+ }
875
+
876
+ /**
877
+ * @notice Batch claim tokens for recipients (each pays for themselves).
878
+ * @param recipients Array of recipient addresses
879
+ * @param uris Array of URIs for each token
880
+ * @param mintRoyalties Array of royalty receivers and percentages
881
+ * Requirements:
882
+ * - All recipients must not have already claimed
883
+ * - All recipients must have approved sufficient payment tokens
884
+ * - Recipients array must not be empty or exceed 100
885
+ * - No zero addresses
886
+ * - Global mint price must be set
887
+ */
888
+ function batchClaim(address[] calldata recipients, string[] calldata uris, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external {
889
+ if (recipients.length == 0) revert EmptyRecipientsArray();
890
+ if (recipients.length > 100) revert TooManyRecipients();
891
+ if (recipients.length != uris.length) revert ArrayLengthMismatch();
892
+ if (mintPrice == 0) revert MintPriceNotSet();
893
+
894
+ for (uint256 i = 0; i < recipients.length; i++) {
895
+ address recipient = recipients[i];
896
+ if (recipient == address(0)) revert InvalidRecipientAddress();
897
+ if (hasClaimed[recipient]) revert RecipientAlreadyClaimed();
898
+ if (bytes(uris[i]).length == 0) revert EmptyTokenURI();
899
+ if(mintPrice > 0) {
900
+ paymentToken.safeTransferFrom(recipient, address(this), mintPrice);
901
+ }
902
+
903
+ // Check total supply limit if set (check before minting each token)
904
+ if (_maxTotalSupply > 0) {
905
+ uint256 currentSupply = totalSupply(); // Get current count from ERC721Enumerable
906
+ if (currentSupply >= _maxTotalSupply) {
907
+ revert TokenSupplyExceeded();
908
+ }
909
+ }
910
+
911
+ uint256 tokenId = _tokenIdCounter;
912
+ _tokenIdCounter++;
913
+ _safeMint(recipient, tokenId);
914
+
915
+ salePrices[tokenId] = mintPrice;
916
+ tokenPrices[tokenId] = mintPrice; // Backward compatibility
917
+ tokenURIs[tokenId] = uris[i];
918
+
919
+ // Calculate and deduct platform commission
920
+ uint96 platformCommission = KamiPlatform.platformCommission();
921
+ uint256 commissionAmount = 0;
922
+ if (platformCommission > 0) {
923
+ commissionAmount = (mintPrice * platformCommission) / 10000;
924
+ if (commissionAmount > 0) {
925
+ paymentToken.safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
926
+ }
927
+ }
928
+
929
+ // Calculate remaining amount after platform commission
930
+ uint256 remainingAmount = mintPrice - commissionAmount;
931
+
932
+ if(mintRoyalties.length > 0) {
933
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
934
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
935
+ }
936
+
937
+ hasClaimed[recipient] = true;
938
+ }
939
+ emit BatchClaimed(msg.sender, recipients);
940
+ }
941
+
942
+ // ============ UPGRADE AUTHORIZATION ============
943
+
944
+ /**
945
+ * @notice Authorize upgrade (OWNER_ROLE only).
946
+ * @param newImplementation Address of the new implementation
947
+ */
948
+ function _authorizeUpgrade(address newImplementation) internal override onlyRole(OWNER_ROLE) {}
949
+ }