@paulstinchcombe/gasless-nft-tx 0.11.3 → 0.12.0

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,1069 @@
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 {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
8
+ import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
9
+ import {ERC1155Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol";
10
+ import {ERC1155SupplyUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.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 KAMI721CUpgradeableOptimized
22
+ * @dev Optimized upgradeable ERC721 contract using split libraries for size efficiency.
23
+ * - Uses split libraries instead of monolithic library for reduced contract size
24
+ * - Supports all features: ERC20 payments, royalties, platform commission, rental functionality
25
+ * - Upgradeable via UUPS proxy pattern
26
+ * - Deployable on size-constrained networks like SONEUM
27
+ *
28
+ * Roles:
29
+ * - OWNER_ROLE: Can manage contract settings
30
+ * - PLATFORM_ROLE: Receives platform commission payments
31
+ * - RENTER_ROLE: Assigned to users renting tokens
32
+ * - UPGRADER_ROLE: Can authorize contract upgrades
33
+ */
34
+ contract KAMI1155CUpgradeable is
35
+ Initializable,
36
+ UUPSUpgradeable,
37
+ AccessControlUpgradeable,
38
+ ERC1155Upgradeable,
39
+ ERC1155SupplyUpgradeable,
40
+ ERC2981Upgradeable,
41
+ PausableUpgradeable
42
+ {
43
+ using SafeERC20 for IERC20;
44
+ // Libraries called explicitly to reduce contract size
45
+
46
+ /// @dev Storage gap for upgradeable contracts
47
+ /// Reserved slots:
48
+ /// - Slots 0-39: Reserved for base contract upgrades
49
+ /// - Slots 40-49: Reserved for future multiple payment token feature (10 slots)
50
+ /// Future storage (commented out, to be uncommented in future upgrade):
51
+ /// - mapping(uint256 => address[]) public tokenPaymentTokens; // Supported payment tokens per token
52
+ /// - mapping(uint256 => mapping(address => uint256)) public tokenPricesByPaymentToken; // Price per token per payment token
53
+ /// - address[] public activePaymentTokens; // Global list of accepted payment tokens
54
+ /// Note: Mappings don't consume storage slots, but we reserve slots for array lengths and configuration flags
55
+ uint256[40] private __gap;
56
+
57
+ /// @notice Role for contract upgrades
58
+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
59
+ /// @notice Role for contract owners (can manage contract settings)
60
+ bytes32 public constant OWNER_ROLE = KamiNFTCore.OWNER_ROLE;
61
+ /// @notice Role for renters (assigned to users renting tokens)
62
+ bytes32 public constant RENTER_ROLE = KamiNFTCore.RENTER_ROLE;
63
+ /// @notice Role for platform (receives commission payments)
64
+ bytes32 public constant PLATFORM_ROLE = KamiNFTCore.PLATFORM_ROLE;
65
+
66
+ event TokenMinted(address minter, uint256 tokenId, uint256 amount, uint256 price);
67
+ event TokenSold(address indexed from, address indexed to, uint256 indexed tokenId, uint256 amount, uint256 price);
68
+ event TokenRented(address renter, address tokenOwner, uint256 tokenId, uint256 startTime, uint256 endTime, uint256 rentalPrice);
69
+ event RentalEnded(uint256 tokenId, address previousRenter, address tokenOwner);
70
+ event RentalExtended(uint256 tokenId, uint256 newEndTime, uint256 newRentalPrice, uint256 additionalPayment);
71
+
72
+ /// @dev Transfer tracker for royalty enforcement
73
+ KamiNFTCore.TransferTracker private _transferTracker;
74
+
75
+ /// @notice ERC20 token used for payments (e.g., USDC)
76
+ IERC20 public paymentToken;
77
+ /// @notice Price in payment token (ERC20) for minting and selling
78
+ mapping(uint256 => uint256) public tokenPrices;
79
+ /// @notice Individual URI for each token
80
+ mapping(uint256 => string) public tokenURIs;
81
+ /// @dev Base URI for token metadata (fallback)
82
+ string private _baseTokenURI;
83
+ /// @dev Counter for token IDs
84
+ uint256 private _tokenIdCounter;
85
+ /// @dev Maximum total supply for each tokenId (0 means unlimited)
86
+ mapping(uint256 => uint256) private _tokenTotalSupplies;
87
+
88
+ /// @dev Actual minted count per tokenId (tracks real supply)
89
+ mapping(uint256 => uint256) private _actualMintedCount;
90
+ /// @dev Maximum total supply for the contract (0 means unlimited)
91
+ uint256 private _maxTotalSupply;
92
+
93
+ /// @dev Custom error for when token supply limit is exceeded
94
+ error TokenSupplyExceeded();
95
+
96
+ /**
97
+ * @dev Disables initializers for implementation contract
98
+ */
99
+ /// @custom:oz-upgrades-unsafe-allow constructor
100
+ constructor() {
101
+ _disableInitializers();
102
+ }
103
+
104
+ /**
105
+ * @notice Initializes the contract (replaces constructor for upgradeable pattern)
106
+ * @param paymentToken_ ERC20 token address for payments
107
+ * @param baseURI_ Base URI for token metadata
108
+ * @param platformAddress_ Address to receive platform commission
109
+ * @param platformCommissionPercentage_ Platform commission (basis points, max 2000 = 20%)
110
+ * @param adminAddress_ Address to receive admin and owner roles
111
+ * @param totalSupply_ Optional total supply limit for the contract (0 means unlimited)
112
+ */
113
+ function initialize(
114
+ address paymentToken_,
115
+ string memory baseURI_,
116
+ address platformAddress_,
117
+ uint96 platformCommissionPercentage_,
118
+ address adminAddress_,
119
+ uint256 totalSupply_
120
+ ) public initializer {
121
+ // Initialize upgradeable contracts
122
+ __UUPSUpgradeable_init();
123
+ __AccessControl_init();
124
+ __ERC1155_init(baseURI_);
125
+ __ERC1155Supply_init();
126
+ __ERC2981_init();
127
+ __Pausable_init();
128
+
129
+ // Validate addresses
130
+ require(paymentToken_ != address(0), "Invalid payment token address");
131
+ require(platformAddress_ != address(0), "Invalid platform address");
132
+ require(adminAddress_ != address(0), "Invalid admin address");
133
+ require(platformCommissionPercentage_ <= 2000, "Platform commission too high");
134
+
135
+ // Set contract state
136
+ paymentToken = IERC20(paymentToken_);
137
+ _baseTokenURI = baseURI_;
138
+
139
+ // Initialize library configurations
140
+ KamiPlatform.initializePlatform(platformAddress_, platformCommissionPercentage_);
141
+ KamiRoyalty.initializeRoyaltyConfig();
142
+
143
+ // Grant roles - use adminAddress_ instead of msg.sender
144
+ _grantRole(DEFAULT_ADMIN_ROLE, adminAddress_);
145
+ _grantRole(OWNER_ROLE, adminAddress_);
146
+ _grantRole(PLATFORM_ROLE, platformAddress_);
147
+ _grantRole(UPGRADER_ROLE, adminAddress_);
148
+
149
+ // Initialize token ID counter
150
+ _tokenIdCounter = 1;
151
+
152
+ // Set total supply if provided (0 means unlimited)
153
+ if (totalSupply_ > 0) {
154
+ _maxTotalSupply = totalSupply_;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * @notice Authorizes contract upgrades (UUPS pattern)
160
+ * @dev Only UPGRADER_ROLE can call.
161
+ */
162
+ function _authorizeUpgrade(address /* newImplementation */) internal view override {
163
+ require(hasRole(UPGRADER_ROLE, msg.sender), "Caller is not an upgrader");
164
+ }
165
+
166
+ /**
167
+ * @notice Checks supported interfaces (ERC721, ERC2981, AccessControl)
168
+ * @param interfaceId Interface identifier
169
+ * @return True if supported
170
+ */
171
+ function supportsInterface(bytes4 interfaceId)
172
+ public
173
+ view
174
+ virtual
175
+ override(ERC1155Upgradeable, ERC2981Upgradeable, AccessControlUpgradeable)
176
+ returns (bool)
177
+ {
178
+ return ERC1155Upgradeable.supportsInterface(interfaceId) ||
179
+ ERC2981Upgradeable.supportsInterface(interfaceId) ||
180
+ AccessControlUpgradeable.supportsInterface(interfaceId);
181
+ }
182
+
183
+ /**
184
+ * @dev Mints a single token
185
+ *
186
+ * Requirements:
187
+ * - Contract must not be paused
188
+ * - Caller must have approved sufficient payment tokens
189
+ * - Caller must have sufficient payment token balance
190
+ */
191
+ function mint(address recipient, uint256 tokenPrice, string calldata tokenURI, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external whenNotPaused {
192
+ require(recipient != address(0), "Recipient cannot be zero address");
193
+ require(bytes(tokenURI).length > 0, "Token URI cannot be empty");
194
+ uint256 amount = 1;
195
+
196
+ uint256 tokenId = _tokenIdCounter;
197
+
198
+ // Check total supply limit if set
199
+ uint256 tokenTotalSupply = _tokenTotalSupplies[tokenId];
200
+ if (tokenTotalSupply > 0) {
201
+ uint256 currentSupply = getTotalMinted(tokenId);
202
+ if (currentSupply + amount > tokenTotalSupply) {
203
+ revert TokenSupplyExceeded();
204
+ }
205
+ }
206
+
207
+ _tokenIdCounter++;
208
+ _mint(recipient, tokenId, amount, "");
209
+
210
+ // Set token price and URI
211
+ tokenPrices[tokenId] = tokenPrice;
212
+ tokenURIs[tokenId] = tokenURI;
213
+
214
+ if(tokenPrice > 0) {
215
+ uint256 totalPrice = tokenPrice * amount;
216
+ require(paymentToken.balanceOf(msg.sender) >= totalPrice, "Insufficient payment token balance");
217
+ require(paymentToken.allowance(msg.sender, address(this)) >= totalPrice, "Insufficient payment token allowance");
218
+ paymentToken.safeTransferFrom(msg.sender, address(this), totalPrice);
219
+
220
+ // Calculate and deduct platform commission
221
+ uint96 platformCommission = KamiPlatform.platformCommission();
222
+ uint256 commissionAmount = 0;
223
+ if (platformCommission > 0) {
224
+ commissionAmount = (totalPrice * platformCommission) / 10000;
225
+ if (commissionAmount > 0) {
226
+ IERC20(address(paymentToken)).safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
227
+ }
228
+ }
229
+
230
+ // Calculate remaining amount after platform commission
231
+ uint256 remainingAmount = totalPrice - commissionAmount;
232
+
233
+ // Set token-specific mint royalties if provided
234
+ if(mintRoyalties.length > 0) {
235
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
236
+ }
237
+
238
+ // Distribute mint royalties on the remaining amount
239
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
240
+ }
241
+
242
+ emit TokenMinted(recipient, tokenId, amount, tokenPrice * amount);
243
+ }
244
+
245
+ /**
246
+ * @dev Mints a specified amount of tokens
247
+ * @param amount The amount of tokens to mint
248
+ *
249
+ * Requirements:
250
+ * - Contract must not be paused
251
+ * - Caller must have approved sufficient payment tokens
252
+ * - Caller must have sufficient payment token balance
253
+ */
254
+ function mintAmount(address recipient, uint256 amount, uint256 tokenPrice, string calldata tokenURI, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external whenNotPaused {
255
+ require(recipient != address(0), "Recipient cannot be zero address");
256
+ require(amount > 0, "Amount must be greater than 0");
257
+ require(bytes(tokenURI).length > 0, "Token URI cannot be empty");
258
+
259
+ uint256 tokenId = _tokenIdCounter;
260
+
261
+ // Check total supply limit if set
262
+ uint256 tokenTotalSupply = _tokenTotalSupplies[tokenId];
263
+ if (tokenTotalSupply > 0) {
264
+ uint256 currentSupply = getTotalMinted(tokenId);
265
+ if (currentSupply + amount > tokenTotalSupply) {
266
+ revert TokenSupplyExceeded();
267
+ }
268
+ }
269
+
270
+ _tokenIdCounter++;
271
+ _mint(recipient, tokenId, amount, "");
272
+
273
+ // Set token price and URI
274
+ tokenPrices[tokenId] = tokenPrice;
275
+ tokenURIs[tokenId] = tokenURI;
276
+
277
+ if(tokenPrice > 0) {
278
+ uint256 totalPrice = tokenPrice * amount;
279
+ require(paymentToken.balanceOf(msg.sender) >= totalPrice, "Insufficient payment token balance");
280
+ require(paymentToken.allowance(msg.sender, address(this)) >= totalPrice, "Insufficient payment token allowance");
281
+ paymentToken.safeTransferFrom(msg.sender, address(this), totalPrice);
282
+
283
+ // Calculate and deduct platform commission
284
+ uint96 platformCommission = KamiPlatform.platformCommission();
285
+ uint256 commissionAmount = 0;
286
+ if (platformCommission > 0) {
287
+ commissionAmount = (totalPrice * platformCommission) / 10000;
288
+ if (commissionAmount > 0) {
289
+ IERC20(address(paymentToken)).safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
290
+ }
291
+ }
292
+
293
+ // Calculate remaining amount after platform commission
294
+ uint256 remainingAmount = totalPrice - commissionAmount;
295
+
296
+ // Set token-specific mint royalties if provided
297
+ if(mintRoyalties.length > 0) {
298
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
299
+ }
300
+
301
+ // Distribute mint royalties on the remaining amount
302
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
303
+ }
304
+
305
+ emit TokenMinted(recipient, tokenId, amount, tokenPrice * amount);
306
+ }
307
+
308
+ /**
309
+ * @dev Mints a specified amount of tokens for a specific recipient
310
+ * @param recipient Address to receive the minted tokens
311
+ * @param amount The amount of tokens to mint
312
+ *
313
+ * Requirements:
314
+ * - Contract must not be paused
315
+ * - Recipient cannot be zero address
316
+ * - Caller must have approved sufficient payment tokens
317
+ * - Caller must have sufficient payment token balance
318
+ */
319
+ function mintFor(address recipient, uint256 amount, uint256 tokenPrice, string calldata tokenURI, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external whenNotPaused {
320
+ require(amount > 0, "Amount must be greater than 0");
321
+ require(recipient != address(0), "Recipient cannot be zero address");
322
+ require(bytes(tokenURI).length > 0, "Token URI cannot be empty");
323
+
324
+ uint256 tokenId = _tokenIdCounter;
325
+
326
+ // Check total supply limit if set
327
+ uint256 tokenTotalSupply = _tokenTotalSupplies[tokenId];
328
+ if (tokenTotalSupply > 0) {
329
+ uint256 currentSupply = getTotalMinted(tokenId);
330
+ if (currentSupply + amount > tokenTotalSupply) {
331
+ revert TokenSupplyExceeded();
332
+ }
333
+ }
334
+
335
+ _tokenIdCounter++;
336
+ _mint(recipient, tokenId, amount, "");
337
+
338
+ // Set token price and URI
339
+ tokenPrices[tokenId] = tokenPrice;
340
+ tokenURIs[tokenId] = tokenURI;
341
+
342
+ if(tokenPrice > 0) {
343
+ uint256 totalPrice = tokenPrice * amount;
344
+ require(paymentToken.balanceOf(msg.sender) >= totalPrice, "Insufficient payment token balance");
345
+ require(paymentToken.allowance(msg.sender, address(this)) >= totalPrice, "Insufficient payment token allowance");
346
+ paymentToken.safeTransferFrom(msg.sender, address(this), totalPrice);
347
+
348
+ // Calculate and deduct platform commission
349
+ uint96 platformCommission = KamiPlatform.platformCommission();
350
+ uint256 commissionAmount = 0;
351
+ if (platformCommission > 0) {
352
+ commissionAmount = (totalPrice * platformCommission) / 10000;
353
+ if (commissionAmount > 0) {
354
+ IERC20(address(paymentToken)).safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
355
+ }
356
+ }
357
+
358
+ // Calculate remaining amount after platform commission
359
+ uint256 remainingAmount = totalPrice - commissionAmount;
360
+
361
+ // Set token-specific mint royalties if provided
362
+ if(mintRoyalties.length > 0) {
363
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
364
+ }
365
+
366
+ // Distribute mint royalties on the remaining amount
367
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
368
+ }
369
+
370
+ emit TokenMinted(recipient, tokenId, amount, tokenPrice * amount);
371
+ }
372
+
373
+ /**
374
+ * @dev Mints multiple tokens in batch
375
+ * @param amounts Array of amounts to mint for each token ID
376
+ *
377
+ * Requirements:
378
+ * - Contract must not be paused
379
+ * - Caller must have approved sufficient payment tokens
380
+ * - Caller must have sufficient payment token balance
381
+ * - All amounts must be greater than 0
382
+ */
383
+ function mintBatch(address[] memory recipients, uint256[] memory amounts, uint256[] memory prices, string[] calldata uris) external whenNotPaused {
384
+ require(recipients.length > 0, "Recipients array cannot be empty");
385
+ require(recipients.length == amounts.length, "Recipients and amounts arrays must have same length");
386
+ require(amounts.length == prices.length, "Amounts and prices arrays must have same length");
387
+ require(amounts.length == uris.length, "Amounts and tokenURIs arrays must have same length");
388
+
389
+ // Very simple implementation for debugging
390
+ for (uint256 i = 0; i < amounts.length; i++) {
391
+ require(recipients[i] != address(0), "Recipient cannot be zero address");
392
+ require(amounts[i] > 0, "Amount must be greater than 0");
393
+ require(prices[i] > 0, "Price must be greater than 0");
394
+ require(bytes(uris[i]).length > 0, "Token URI cannot be empty");
395
+ uint256 tokenId = _tokenIdCounter;
396
+
397
+ // Check total supply limit if set
398
+ uint256 tokenTotalSupply = _tokenTotalSupplies[tokenId];
399
+ if (tokenTotalSupply > 0) {
400
+ uint256 currentSupply = getTotalMinted(tokenId);
401
+ if (currentSupply + amounts[i] > tokenTotalSupply) {
402
+ revert TokenSupplyExceeded();
403
+ }
404
+ }
405
+
406
+ _tokenIdCounter++;
407
+ _mint(recipients[i], tokenId, amounts[i], "");
408
+ tokenPrices[tokenId] = prices[i];
409
+ tokenURIs[tokenId] = uris[i];
410
+ emit TokenMinted(recipients[i], tokenId, amounts[i], prices[i] * amounts[i]);
411
+ }
412
+ }
413
+
414
+ /**
415
+ * @dev Sets the price
416
+ * @param newPrice The new price in payment token units
417
+ *
418
+ * Requirements:
419
+ * - Caller must have OWNER_ROLE
420
+ */
421
+ function setPrice(uint256 tokenId, uint256 newPrice) external {
422
+ require(hasRole(OWNER_ROLE, msg.sender), "Caller is not an owner");
423
+ require(exists(tokenId), "Token does not exist");
424
+ tokenPrices[tokenId] = newPrice;
425
+ }
426
+
427
+ /**
428
+ * @dev Sets the URI for a specific token
429
+ * @param tokenId The token ID
430
+ * @param newTokenURI The new URI for the token's metadata
431
+ *
432
+ * Requirements:
433
+ * - Caller must have OWNER_ROLE
434
+ * - Token must exist
435
+ * - Token URI cannot be empty
436
+ */
437
+ function setTokenURI(uint256 tokenId, string calldata newTokenURI) external {
438
+ require(hasRole(OWNER_ROLE, msg.sender), "Caller is not an owner");
439
+ require(exists(tokenId), "Token does not exist");
440
+ require(bytes(newTokenURI).length > 0, "Token URI cannot be empty");
441
+ tokenURIs[tokenId] = newTokenURI;
442
+ }
443
+
444
+ /**
445
+ * @notice Set base URI (only owner)
446
+ * @param newBaseURI New base URI
447
+ */
448
+ function setBaseURI(string memory newBaseURI) external onlyRole(OWNER_ROLE) {
449
+ _baseTokenURI = newBaseURI;
450
+ }
451
+
452
+ /**
453
+ * @dev Returns the URI for a given token ID
454
+ * @dev Returns individual token URI if set, otherwise falls back to base URI
455
+ * @param tokenId The token ID to query
456
+ * @return The token URI
457
+ */
458
+ function uri(uint256 tokenId) public view override returns (string memory) {
459
+ require(exists(tokenId), "URI query for nonexistent token");
460
+
461
+ string memory individualURI = tokenURIs[tokenId];
462
+ if (bytes(individualURI).length > 0) {
463
+ return individualURI;
464
+ }
465
+
466
+ return string(abi.encodePacked(_baseTokenURI, Strings.toString(tokenId)));
467
+ }
468
+
469
+ /**
470
+ * @notice Get base URI
471
+ * @return Base URI string
472
+ */
473
+ function _baseURI() internal view returns (string memory) {
474
+ return _baseTokenURI;
475
+ }
476
+
477
+ /**
478
+ * @notice Pause contract (only owner)
479
+ */
480
+ function pause() external onlyRole(OWNER_ROLE) {
481
+ _pause();
482
+ }
483
+
484
+ /**
485
+ * @notice Unpause contract (only owner)
486
+ */
487
+ function unpause() external onlyRole(OWNER_ROLE) {
488
+ _unpause();
489
+ }
490
+
491
+ modifier onlyOwnerOrRenter(uint256 tokenId) {
492
+ mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals();
493
+ require(
494
+ balanceOf(msg.sender, tokenId) > 0 || // Owner check
495
+ rentals[tokenId].renter == msg.sender, // Renter check
496
+ "Must own tokens or be renter to end rental"
497
+ );
498
+ _;
499
+ }
500
+
501
+ /**
502
+ * @notice Set royalty percentage (only owner)
503
+ * @param percentage Royalty percentage in basis points (max 2000 = 20%)
504
+ */
505
+ function setRoyaltyPercentage(uint96 percentage) external onlyRole(OWNER_ROLE) {
506
+ KamiRoyalty.setRoyaltyPercentage(percentage);
507
+ }
508
+
509
+ /**
510
+ * @notice Get royalty percentage
511
+ * @return Royalty percentage in basis points
512
+ */
513
+ function royaltyPercentage() external view returns (uint96) {
514
+ return KamiRoyalty.royaltyPercentage();
515
+ }
516
+
517
+ /**
518
+ * @notice Get platform commission percentage
519
+ * @return Platform commission percentage in basis points
520
+ */
521
+ function platformCommission() external view returns (uint96) {
522
+ return KamiPlatform.platformCommission();
523
+ }
524
+
525
+ /**
526
+ * @notice Get platform address
527
+ * @return Platform address
528
+ */
529
+ function platformAddress() external view returns (address) {
530
+ return KamiPlatform.platformAddress();
531
+ }
532
+
533
+ /**
534
+ * @dev Sets global mint royalties
535
+ * @param royalties Array of royalty data containing receiver addresses and fee numerators
536
+ *
537
+ * Requirements:
538
+ * - Caller must have OWNER_ROLE
539
+ */
540
+ function setMintRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external {
541
+ require(hasRole(OWNER_ROLE, msg.sender), "Caller is not an owner");
542
+ KamiRoyalty.setMintRoyalties(royalties);
543
+ }
544
+
545
+ /**
546
+ * @dev Sets global transfer royalties
547
+ * @param royalties Array of royalty data containing receiver addresses and fee numerators
548
+ *
549
+ * Requirements:
550
+ * - Caller must have OWNER_ROLE
551
+ */
552
+ function setTransferRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external {
553
+ require(hasRole(OWNER_ROLE, msg.sender), "Caller is not an owner");
554
+ KamiRoyalty.setTransferRoyalties(royalties);
555
+ }
556
+
557
+ /**
558
+ * @notice Get mint royalty receivers for a token
559
+ * @param tokenId Token ID
560
+ * @return Array of royalty receivers
561
+ */
562
+ function getMintRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
563
+ return KamiRoyalty.getMintRoyaltyReceivers(tokenId);
564
+ }
565
+
566
+ /**
567
+ * @notice Get transfer royalty receivers for a token
568
+ * @param tokenId Token ID
569
+ * @return Array of royalty receivers
570
+ */
571
+ function getTransferRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
572
+ return KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
573
+ }
574
+
575
+ /**
576
+ * @notice Royalty info for a token sale (ERC2981).
577
+ * @dev Price is automatically taken from tokenPrices mapping, price parameter is unused but required for ERC2981 interface
578
+ * @param tokenId Token ID
579
+ * @return receiver Royalty receiver (first receiver for ERC2981 compatibility)
580
+ * @return royaltyAmount Royalty amount (first receiver's share)
581
+ */
582
+ function royaltyInfo(uint256 tokenId, uint256 /* salePrice */)
583
+ public
584
+ view
585
+ override
586
+ returns (address receiver, uint256 royaltyAmount)
587
+ {
588
+ uint256 price = tokenPrices[tokenId];
589
+ if (price == 0) {
590
+ return (address(0), 0);
591
+ }
592
+
593
+ uint256 totalRoyaltyAmount = (price * KamiRoyalty.royaltyPercentage()) / 10000;
594
+ KamiNFTCore.RoyaltyData[] memory royalties = KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
595
+ if (royalties.length > 0) {
596
+ KamiNFTCore.RoyaltyData memory info = royalties[0];
597
+ uint256 receiverShare = (totalRoyaltyAmount * info.feeNumerator) / 10000;
598
+ return (info.receiver, receiverShare);
599
+ }
600
+ return (address(0), 0);
601
+ }
602
+
603
+ /**
604
+ * @notice Get all royalty receivers and their amounts for a token sale.
605
+ * @param tokenId Token ID
606
+ * @return receivers Array of royalty receiver addresses
607
+ * @return amounts Array of royalty amounts for each receiver
608
+ */
609
+ function getRoyaltyInfo(uint256 tokenId)
610
+ public
611
+ view
612
+ returns (address[] memory receivers, uint256[] memory amounts)
613
+ {
614
+ uint256 price = tokenPrices[tokenId];
615
+ if (price == 0) {
616
+ return (new address[](0), new uint256[](0));
617
+ }
618
+
619
+ uint256 totalRoyaltyAmount = (price * KamiRoyalty.royaltyPercentage()) / 10000;
620
+ KamiNFTCore.RoyaltyData[] memory royalties = KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
621
+
622
+ if (royalties.length == 0) {
623
+ return (new address[](0), new uint256[](0));
624
+ }
625
+
626
+ receivers = new address[](royalties.length);
627
+ amounts = new uint256[](royalties.length);
628
+
629
+ for (uint256 i = 0; i < royalties.length; i++) {
630
+ receivers[i] = royalties[i].receiver;
631
+ amounts[i] = (totalRoyaltyAmount * royalties[i].feeNumerator) / 10000;
632
+ }
633
+
634
+ return (receivers, amounts);
635
+ }
636
+
637
+ /**
638
+ * @notice Update platform commission
639
+ * @param newPlatformCommissionPercentage New platform commission percentage
640
+ * @param newPlatformAddress New platform address
641
+ */
642
+ function setPlatformCommission(
643
+ uint96 newPlatformCommissionPercentage,
644
+ address newPlatformAddress
645
+ ) external onlyRole(OWNER_ROLE) {
646
+ // Get the current platform address before updating it in KamiPlatform
647
+ address oldPlatformAddress = KamiPlatform.platformAddress();
648
+
649
+ KamiPlatform.updatePlatformCommission(
650
+ newPlatformCommissionPercentage,
651
+ newPlatformAddress,
652
+ address(this)
653
+ );
654
+
655
+ // Revoke PLATFORM_ROLE from the old platform address
656
+ _revokeRole(PLATFORM_ROLE, oldPlatformAddress);
657
+
658
+ // Grant PLATFORM_ROLE to the new platform address
659
+ _grantRole(PLATFORM_ROLE, newPlatformAddress);
660
+ }
661
+
662
+ /**
663
+ * @notice Set token-specific mint royalties
664
+ * @param tokenId The token ID to set royalties for
665
+ * @param royalties Array of royalty receivers and percentages
666
+ */
667
+ function setTokenMintRoyalties(
668
+ uint256 tokenId,
669
+ KamiNFTCore.RoyaltyData[] calldata royalties
670
+ ) external onlyRole(OWNER_ROLE) {
671
+ KamiRoyalty.setTokenMintRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
672
+ }
673
+
674
+ /**
675
+ * @notice Set token-specific transfer royalties
676
+ * @param tokenId The token ID to set royalties for
677
+ * @param royalties Array of royalty receivers and percentages
678
+ */
679
+ function setTokenTransferRoyalties(
680
+ uint256 tokenId,
681
+ KamiNFTCore.RoyaltyData[] calldata royalties
682
+ ) external onlyRole(OWNER_ROLE) {
683
+ KamiRoyalty.setTokenTransferRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
684
+ }
685
+
686
+
687
+ /**
688
+ * @notice Rent a token
689
+ * @param tokenId Token ID to rent
690
+ * @param duration Rental duration in seconds
691
+ * @param rentalPrice Total rental price
692
+ */
693
+ function rentToken(
694
+ uint256 tokenId,
695
+ uint256 duration,
696
+ uint256 rentalPrice,
697
+ address renter,
698
+ address tokenOwner
699
+ ) external whenNotPaused {
700
+ require(exists(tokenId), "Token does not exist");
701
+ require(renter != address(0), "Renter cannot be zero address");
702
+ require(tokenOwner != address(0), "Token owner cannot be zero address");
703
+ require(tokenOwner != renter, "Owner cannot rent their own token");
704
+ require(balanceOf(tokenOwner, tokenId) > 0, "Token owner does not have the token");
705
+
706
+ KamiRental.rentToken(
707
+ paymentToken,
708
+ tokenId,
709
+ duration,
710
+ rentalPrice,
711
+ tokenOwner,
712
+ msg.sender // payer
713
+ );
714
+ emit TokenRented(renter, tokenOwner, tokenId, KamiRental.getRentalInfo(tokenId).startTime, KamiRental.getRentalInfo(tokenId).endTime, rentalPrice);
715
+ }
716
+
717
+ /**
718
+ * @notice End a rental early
719
+ * @param tokenId Token ID to end rental for
720
+ */
721
+ function endRental(uint256 tokenId) external onlyOwnerOrRenter(tokenId) {
722
+ require(exists(tokenId), "Token does not exist");
723
+
724
+ KamiRental.endRentalSimple(tokenId);
725
+ KamiNFTCore.Rental memory rentalInfo = KamiRental.getRentalInfo(tokenId);
726
+ emit RentalEnded(tokenId, rentalInfo.renter, msg.sender);
727
+ }
728
+
729
+ /**
730
+ * @notice Extend a rental period
731
+ * @param tokenId Token ID to extend rental for
732
+ * @param additionalDuration Additional duration in seconds
733
+ * @param additionalPayment Additional payment required
734
+ */
735
+ function extendRental(
736
+ uint256 tokenId,
737
+ uint256 additionalDuration,
738
+ uint256 additionalPayment
739
+ ) external whenNotPaused {
740
+ require(exists(tokenId), "Token does not exist");
741
+
742
+ KamiRental.extendRental(
743
+ paymentToken,
744
+ tokenId,
745
+ additionalDuration,
746
+ additionalPayment,
747
+ msg.sender, // token owner
748
+ msg.sender // payer
749
+ );
750
+ KamiNFTCore.Rental memory updatedRental = KamiRental.getRentalInfo(tokenId);
751
+ emit RentalExtended(tokenId, updatedRental.endTime, updatedRental.rentalPrice, additionalPayment);
752
+ }
753
+
754
+ /**
755
+ * @notice Get rental information for a token
756
+ * @param tokenId Token ID to get rental info for
757
+ * @return Rental information
758
+ */
759
+ function getRentalInfo(uint256 tokenId) external view returns (KamiNFTCore.Rental memory) {
760
+ return KamiRental.getRentalInfo(tokenId);
761
+ }
762
+
763
+ /**
764
+ * @notice Check if a token is currently rented
765
+ * @param tokenId Token ID to check
766
+ * @return True if the token is rented, false otherwise
767
+ */
768
+ function isRented(uint256 tokenId) external view returns (bool) {
769
+ return KamiRental.isRented(tokenId);
770
+ }
771
+
772
+ /**
773
+ * @dev Helper function to check if a token exists
774
+ * @param tokenId The token ID to check
775
+ * @return True if the token exists, false otherwise
776
+ */
777
+ function _exists(uint256 tokenId) internal view returns (bool) {
778
+ // Use the actual minted count, not the limit
779
+ return getTotalMinted(tokenId) > 0;
780
+ }
781
+
782
+ /**
783
+ * @dev Public function to check if a token exists (for library compatibility)
784
+ * @param tokenId The token ID to check
785
+ * @return True if the token exists, false otherwise
786
+ */
787
+ function exists(uint256 tokenId) public view override returns (bool) {
788
+ // Use the actual minted count, not the limit
789
+ return getTotalMinted(tokenId) > 0;
790
+ }
791
+
792
+ /**
793
+ * @notice Override totalSupply to return the set limit instead of actual minted amount
794
+ * @param tokenId Token ID
795
+ * @return Total supply limit (0 means unlimited)
796
+ */
797
+ function totalSupply(uint256 tokenId) public view override returns (uint256) {
798
+ return _tokenTotalSupplies[tokenId];
799
+ }
800
+
801
+ /**
802
+ * @notice Set total supply limit for the contract (OWNER_ROLE only).
803
+ * @param maxSupply Maximum total supply for the contract (0 means unlimited)
804
+ */
805
+ function setTotalSupply(uint256 maxSupply) external onlyRole(OWNER_ROLE) {
806
+ _maxTotalSupply = maxSupply;
807
+ }
808
+
809
+ /**
810
+ * @notice Get total supply limit for the contract.
811
+ * @return Total supply limit (0 means unlimited)
812
+ */
813
+ function maxTotalSupply() public view returns (uint256) {
814
+ return _maxTotalSupply;
815
+ }
816
+
817
+ /**
818
+ * @notice Set total supply limit for a specific tokenId (OWNER_ROLE only).
819
+ * @param tokenId Token ID
820
+ * @param maxSupply Maximum number of tokens that can be minted for this tokenId
821
+ */
822
+ function setTokenTotalSupply(uint256 tokenId, uint256 maxSupply) external onlyRole(OWNER_ROLE) {
823
+ _tokenTotalSupplies[tokenId] = maxSupply;
824
+ }
825
+
826
+ /**
827
+ * @notice Get the number of tokens minted so far for a specific tokenId.
828
+ * @param tokenId Token ID
829
+ * @return Number of tokens minted
830
+ */
831
+ function getTotalMinted(uint256 tokenId) public view returns (uint256) {
832
+ // Use our tracked count which is maintained in _update
833
+ // Since we track in _update which is called for all mints/burns, this should always be accurate
834
+ return _actualMintedCount[tokenId];
835
+ }
836
+
837
+ /**
838
+ * @dev Wrapper function for totalSupply to match library expectations
839
+ * @return The total number of unique tokens minted
840
+ */
841
+ function _getTotalSupply() public view returns (uint256) {
842
+ return _tokenIdCounter;
843
+ }
844
+
845
+ /**
846
+ * @dev Dummy tokenByIndex function for ERC1155 (not used, but required for library compatibility)
847
+ * @return Always reverts
848
+ */
849
+ function _dummyTokenByIndex(uint256) public pure returns (uint256) {
850
+ revert("tokenByIndex not supported in ERC1155");
851
+ }
852
+
853
+ /**
854
+ * @dev Dummy getApproved function for ERC1155 (not used, but required for library compatibility)
855
+ * @return Always returns address(0)
856
+ */
857
+ function _dummyGetApproved(uint256) public pure returns (address) {
858
+ return address(0);
859
+ }
860
+
861
+ /**
862
+ * @dev Burns a specified amount of tokens
863
+ * @param tokenId The token ID to burn
864
+ * @param amount The amount of tokens to burn
865
+ *
866
+ * Requirements:
867
+ * - Contract must not be paused
868
+ * - Caller must own sufficient tokens
869
+ * - Token must not be rented
870
+ */
871
+ function burn(uint256 tokenId, uint256 amount) external whenNotPaused {
872
+ require(amount > 0, "Amount must be greater than 0");
873
+ require(balanceOf(msg.sender, tokenId) >= amount, "Insufficient token balance");
874
+
875
+ // Check if token is rented
876
+ if (KamiRental.isRented(tokenId)) {
877
+ mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals();
878
+ require(rentals[tokenId].renter == msg.sender, "Cannot burn rented token");
879
+ }
880
+
881
+ _burn(msg.sender, tokenId, amount);
882
+ }
883
+
884
+ /**
885
+ * @dev Burn a single token (convenience function)
886
+ * Requirements:
887
+ * - Caller must own the token
888
+ * - Token must not be rented
889
+ */
890
+ function burn(uint256 tokenId) external whenNotPaused {
891
+ require(balanceOf(msg.sender, tokenId) > 0, "Insufficient token balance");
892
+
893
+ // Check if token is rented
894
+ if (KamiRental.isRented(tokenId)) {
895
+ mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals();
896
+ require(rentals[tokenId].renter == msg.sender, "Cannot burn rented token");
897
+ }
898
+
899
+ _burn(msg.sender, tokenId, 1);
900
+ }
901
+
902
+ /**
903
+ * @dev Burns multiple tokens in batch
904
+ * @param tokenIds Array of token IDs to burn
905
+ * @param amounts Array of amounts to burn for each token ID
906
+ *
907
+ * Requirements:
908
+ * - Contract must not be paused
909
+ * - Caller must own sufficient tokens for each token ID
910
+ * - No token must be rented
911
+ */
912
+ function burnBatch(uint256[] memory tokenIds, uint256[] memory amounts) external whenNotPaused {
913
+ require(tokenIds.length == amounts.length, "Arrays length mismatch");
914
+ require(tokenIds.length > 0, "Arrays cannot be empty");
915
+
916
+ for (uint256 i = 0; i < tokenIds.length; i++) {
917
+ require(amounts[i] > 0, "Amount must be greater than 0");
918
+ require(balanceOf(msg.sender, tokenIds[i]) >= amounts[i], "Insufficient token balance");
919
+ }
920
+ _burnBatch(msg.sender, tokenIds, amounts);
921
+ }
922
+
923
+ /**
924
+ * @notice Initiate transfer with royalty requirement
925
+ * @param to The recipient address
926
+ * @param tokenId The token ID to transfer
927
+ * @param price The price
928
+ */
929
+ function initiateTransferWithRoyalty(
930
+ address to,
931
+ uint256 tokenId,
932
+ uint256 price
933
+ ) external {
934
+ require(exists(tokenId), "Token does not exist");
935
+ require(balanceOf(msg.sender, tokenId) > 0, "Not token owner");
936
+
937
+ KamiTransfer.initiateTransferWithRoyalty(tokenId, to, price, msg.sender);
938
+ }
939
+
940
+ /**
941
+ * @notice Pay transfer royalty
942
+ * @param tokenId The token ID being transferred
943
+ * @param price The price
944
+ */
945
+ function payTransferRoyalty(
946
+ address seller,
947
+ uint256 tokenId,
948
+ uint256 price
949
+ ) external {
950
+ require(exists(tokenId), "Token does not exist");
951
+
952
+ // The buyer (msg.sender) pays the seller for the token
953
+ // For ERC1155, the seller is passed as a parameter since there's no single owner concept
954
+ KamiTransfer.payTransferRoyalty(paymentToken, tokenId, price, seller, msg.sender);
955
+ }
956
+
957
+ /**
958
+ * @notice Check if transfer royalty is required
959
+ * @param tokenId The token ID being transferred
960
+ * @param price The price
961
+ * @return True if transfer royalty is required, false otherwise
962
+ */
963
+ function isTransferRoyaltyRequired(
964
+ address /* from */,
965
+ address /* to */,
966
+ uint256 tokenId,
967
+ uint256 price
968
+ ) external view returns (bool) {
969
+ return KamiTransfer.isTransferRoyaltyRequired(tokenId, price);
970
+ }
971
+
972
+ /**
973
+ * @dev Sells tokens with automatic royalty distribution
974
+ * @param to The address to sell to
975
+ * @param tokenId The token ID to sell
976
+ * @param amount The amount of tokens to sell
977
+ *
978
+ * Requirements:
979
+ * - Contract must not be paused
980
+ * - Caller must own sufficient tokens
981
+ * - Token must not be rented
982
+ */
983
+ function sellToken(address to, uint256 tokenId, uint256 amount, address seller) external whenNotPaused {
984
+ require(to != address(0), "Cannot sell to zero address");
985
+ require(seller != address(0), "Seller cannot be zero address");
986
+ require(amount > 0, "Amount must be greater than 0");
987
+
988
+ // Check that the seller owns the token
989
+ require(balanceOf(seller, tokenId) >= amount, "Insufficient token balance");
990
+
991
+ uint256 price = tokenPrices[tokenId];
992
+ require(price > 0, "Token price not set");
993
+
994
+ // Process sale with royalties FIRST (marks transfer as paid)
995
+ KamiTransfer.sellToken(paymentToken, tokenId, to, price, seller);
996
+
997
+ // Then transfer token (will now pass validation)
998
+ safeTransferFrom(seller, to, tokenId, amount, "");
999
+
1000
+ emit TokenSold(seller, to, tokenId, amount, price);
1001
+ }
1002
+
1003
+ /**
1004
+ * @dev Checks if a user has any active rentals
1005
+ * @param user The address to check
1006
+ * @return True if the user has active rentals, false otherwise
1007
+ */
1008
+ function hasActiveRentals(address user) public view whenNotPaused returns (bool) {
1009
+ // For ERC1155, we need to check all possible token IDs
1010
+ return KamiRental.hasActiveRentalsERC1155(user, _tokenIdCounter);
1011
+ }
1012
+
1013
+ function _update(
1014
+ address from,
1015
+ address to,
1016
+ uint256[] memory ids,
1017
+ uint256[] memory amounts
1018
+ ) internal override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) {
1019
+ super._update(from, to, ids, amounts);
1020
+
1021
+ address operator = msg.sender;
1022
+ bytes memory data = "";
1023
+
1024
+ for (uint256 i = 0; i < ids.length; i++) {
1025
+ uint256 tokenId = ids[i];
1026
+ uint256 amount = amounts[i];
1027
+
1028
+ // Track actual minted count
1029
+ if (from == address(0)) {
1030
+ // Minting
1031
+ _actualMintedCount[tokenId] += amount;
1032
+ } else if (to == address(0)) {
1033
+ // Burning
1034
+ _actualMintedCount[tokenId] -= amount;
1035
+ }
1036
+
1037
+ if (from != address(0) && to != address(0)) {
1038
+ // If the token is being transferred FROM an address (not being minted from address(0))
1039
+ // and TO an address (not being burned to address(0))
1040
+ KamiNFTCore.Rental memory rental = KamiRental.getRentalInfo(tokenId);
1041
+ if (rental.active) {
1042
+ revert("Cannot transfer actively rented token"); // Revert if rented, regardless of renter
1043
+ }
1044
+ }
1045
+
1046
+ // Validate transfer if not minting or burning
1047
+ if (from != address(0) && to != address(0)) {
1048
+ KamiTransfer.validateTransfer(
1049
+ tokenId,
1050
+ from,
1051
+ to,
1052
+ isApprovedForAll
1053
+ );
1054
+ // Update rental status on transfer
1055
+ KamiTransfer.updateRentalOnTransferSimple(tokenId);
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // Rental updates are now handled in _update function
1061
+
1062
+ /**
1063
+ * @dev Returns an external view function that checks if a token exists.
1064
+ * This is used for compatibility with library functions that require an external function pointer.
1065
+ */
1066
+ function _getExistsReference() internal view returns (function(uint256) external view returns (bool)) {
1067
+ return this.exists; // Directly return the external view function pointer to the contract's exists
1068
+ }
1069
+ }