@paulstinchcombe/gasless-nft-tx 0.11.2 → 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 (23) 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-sponsored-operations.d.ts.map +1 -1
  17. package/dist/kami-sponsored-operations.js +19 -7
  18. package/dist/kami-sponsored-operations.js.map +1 -1
  19. package/dist/kami-upgrade-manager.d.ts +65 -0
  20. package/dist/kami-upgrade-manager.d.ts.map +1 -0
  21. package/dist/kami-upgrade-manager.js +320 -0
  22. package/dist/kami-upgrade-manager.js.map +1 -0
  23. package/package.json +1 -1
@@ -0,0 +1,691 @@
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 {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.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 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 KAMI721CUpgradeable is
35
+ Initializable,
36
+ UUPSUpgradeable,
37
+ AccessControlUpgradeable,
38
+ ERC721EnumerableUpgradeable,
39
+ ERC2981Upgradeable,
40
+ PausableUpgradeable
41
+ {
42
+ using SafeERC20 for IERC20;
43
+ // Libraries called explicitly to reduce contract size
44
+
45
+ /// @dev Storage gap for upgradeable contracts
46
+ /// Reserved slots:
47
+ /// - Slots 0-39: Reserved for base contract upgrades
48
+ /// - Slots 40-49: Reserved for future multiple payment token feature (10 slots)
49
+ /// Future storage (commented out, to be uncommented in future upgrade):
50
+ /// - mapping(uint256 => address[]) public tokenPaymentTokens; // Supported payment tokens per token
51
+ /// - mapping(uint256 => mapping(address => uint256)) public tokenPricesByPaymentToken; // Price per token per payment token
52
+ /// - address[] public activePaymentTokens; // Global list of accepted payment tokens
53
+ /// Note: Mappings don't consume storage slots, but we reserve slots for array lengths and configuration flags
54
+ uint256[40] private __gap;
55
+
56
+ /// @notice Role for contract upgrades
57
+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
58
+ /// @notice Role for contract owners (can manage contract settings)
59
+ bytes32 public constant OWNER_ROLE = KamiNFTCore.OWNER_ROLE;
60
+ /// @notice Role for renters (assigned to users renting tokens)
61
+ bytes32 public constant RENTER_ROLE = KamiNFTCore.RENTER_ROLE;
62
+ /// @notice Role for platform (receives commission payments)
63
+ bytes32 public constant PLATFORM_ROLE = KamiNFTCore.PLATFORM_ROLE;
64
+
65
+ /// @dev Transfer tracker for royalty enforcement
66
+ KamiNFTCore.TransferTracker private _transferTracker;
67
+
68
+ /// @notice ERC20 token used for payments (e.g., USDC)
69
+ IERC20 public paymentToken;
70
+ /// @notice Price in payment token (ERC20) for each token
71
+ mapping(uint256 => uint256) public tokenPrices;
72
+ /// @notice Individual URI for each token
73
+ mapping(uint256 => string) public tokenURIs;
74
+ /// @dev Base URI for token metadata (fallback)
75
+ string private _baseTokenURI;
76
+ /// @dev Counter for token IDs
77
+ uint256 private _tokenIdCounter;
78
+
79
+ // Added for upgradeable support for exists method to be available as an external function pointer
80
+ function exists(uint256 tokenId) public view returns (bool) {
81
+ try this.ownerOf(tokenId) returns (address) {
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @dev Returns an internal view function to check if an operator is approved for all tokens.
90
+ * This is used for compatibility with library functions that require an internal function pointer.
91
+ */
92
+ function _getIsApprovedForAllReference() internal view returns (function(address, address) view returns (bool)) {
93
+ return isApprovedForAll;
94
+ }
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 name_ ERC721 token name
108
+ * @param symbol_ ERC721 token symbol
109
+ * @param baseURI_ Base URI for token metadata
110
+ * @param platformAddress_ Address to receive platform commission
111
+ * @param platformCommissionPercentage_ Platform commission (basis points, max 2000 = 20%)
112
+ */
113
+ function initialize(
114
+ address paymentToken_,
115
+ string memory name_,
116
+ string memory symbol_,
117
+ string memory baseURI_,
118
+ address platformAddress_,
119
+ uint96 platformCommissionPercentage_,
120
+ address adminAddress_
121
+ ) public initializer {
122
+ // Initialize upgradeable contracts
123
+ __UUPSUpgradeable_init();
124
+ __AccessControl_init();
125
+ __ERC721_init(name_, symbol_);
126
+ __ERC721Enumerable_init();
127
+ __ERC2981_init();
128
+ __Pausable_init();
129
+
130
+ // Validate addresses
131
+ require(paymentToken_ != address(0), "Invalid payment token address");
132
+ require(platformAddress_ != address(0), "Invalid platform address");
133
+ require(adminAddress_ != address(0), "Invalid admin address");
134
+ require(platformCommissionPercentage_ <= 2000, "Platform commission too high");
135
+
136
+ // Set contract state
137
+ paymentToken = IERC20(paymentToken_);
138
+ _baseTokenURI = baseURI_;
139
+
140
+ // Initialize library configurations
141
+ KamiPlatform.initializePlatform(platformAddress_, platformCommissionPercentage_);
142
+ KamiRoyalty.initializeRoyaltyConfig();
143
+
144
+ // Grant roles - use adminAddress_ instead of msg.sender
145
+ _grantRole(DEFAULT_ADMIN_ROLE, adminAddress_);
146
+ _grantRole(OWNER_ROLE, adminAddress_);
147
+ _grantRole(PLATFORM_ROLE, platformAddress_);
148
+ _grantRole(UPGRADER_ROLE, adminAddress_);
149
+
150
+ // Initialize token ID counter
151
+ _tokenIdCounter = 1;
152
+ }
153
+
154
+ /**
155
+ * @notice Authorizes contract upgrades (UUPS pattern)
156
+ * @dev Only UPGRADER_ROLE can call.
157
+ */
158
+ function _authorizeUpgrade(address /* newImplementation */) internal view override {
159
+ require(hasRole(UPGRADER_ROLE, msg.sender), "Caller is not an upgrader");
160
+ }
161
+
162
+ /**
163
+ * @notice Checks supported interfaces (ERC721, ERC2981, AccessControl)
164
+ * @param interfaceId Interface identifier
165
+ * @return True if supported
166
+ */
167
+ function supportsInterface(bytes4 interfaceId)
168
+ public
169
+ view
170
+ virtual
171
+ override(ERC721EnumerableUpgradeable, ERC2981Upgradeable, AccessControlUpgradeable)
172
+ returns (bool)
173
+ {
174
+ return ERC721EnumerableUpgradeable.supportsInterface(interfaceId) ||
175
+ ERC2981Upgradeable.supportsInterface(interfaceId) ||
176
+ AccessControlUpgradeable.supportsInterface(interfaceId);
177
+ }
178
+
179
+ /**
180
+ * @notice Mint a new token
181
+ * @dev Requires payment in the specified ERC20 token
182
+ * @param recipient Address to receive the minted token
183
+ * @param tokenPrice Price for this specific token
184
+ * @param uri Individual URI for this token's metadata
185
+ */
186
+ function mint(address recipient, uint256 tokenPrice, string calldata uri, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external whenNotPaused {
187
+ require(recipient != address(0), "Recipient cannot be zero address");
188
+ require(bytes(uri).length > 0, "Token URI cannot be empty");
189
+
190
+ // Mint token
191
+ uint256 tokenId = _tokenIdCounter;
192
+ _tokenIdCounter++;
193
+ _safeMint(recipient, tokenId);
194
+
195
+ // Set token price and URI
196
+ tokenPrices[tokenId] = tokenPrice;
197
+ tokenURIs[tokenId] = uri;
198
+
199
+ if(tokenPrice > 0) {
200
+ // Transfer payment to contract
201
+ paymentToken.safeTransferFrom(msg.sender, address(this), tokenPrice);
202
+
203
+ // Calculate and deduct platform commission
204
+ uint96 platformCommission = KamiPlatform.platformCommission();
205
+ uint256 commissionAmount = 0;
206
+ if (platformCommission > 0) {
207
+ commissionAmount = (tokenPrice * platformCommission) / 10000;
208
+ if (commissionAmount > 0) {
209
+ IERC20(address(paymentToken)).safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
210
+ }
211
+ }
212
+
213
+ // Calculate remaining amount after platform commission
214
+ uint256 remainingAmount = tokenPrice - commissionAmount;
215
+
216
+ // Set token-specific mint royalties if provided
217
+ if(mintRoyalties.length > 0) {
218
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
219
+ }
220
+
221
+ // Distribute mint royalties on the remaining amount
222
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
223
+ }
224
+ }
225
+
226
+ /**
227
+ * @notice Mint a new token for a specific recipient
228
+ * @dev Requires payment in the specified ERC20 token from msg.sender
229
+ * @param recipient Address to receive the minted token
230
+ * @param tokenPrice Price for this specific token
231
+ * @param uri Individual URI for this token's metadata
232
+ */
233
+ function mintFor(address recipient, uint256 tokenPrice, string calldata uri, KamiNFTCore.RoyaltyData[] calldata mintRoyalties) external whenNotPaused {
234
+ require(recipient != address(0), "Recipient cannot be zero address");
235
+ require(bytes(uri).length > 0, "Token URI cannot be empty");
236
+
237
+ // Mint token to recipient
238
+ uint256 tokenId = _tokenIdCounter;
239
+ _tokenIdCounter++;
240
+ _safeMint(recipient, tokenId);
241
+
242
+ // Set token price and URI
243
+ tokenPrices[tokenId] = tokenPrice;
244
+ tokenURIs[tokenId] = uri;
245
+
246
+ if(tokenPrice > 0) {
247
+ // Transfer payment from caller
248
+ paymentToken.safeTransferFrom(msg.sender, address(this), tokenPrice);
249
+
250
+ // Calculate and deduct platform commission
251
+ uint96 platformCommission = KamiPlatform.platformCommission();
252
+ uint256 commissionAmount = 0;
253
+ if (platformCommission > 0) {
254
+ commissionAmount = (tokenPrice * platformCommission) / 10000;
255
+ if (commissionAmount > 0) {
256
+ IERC20(address(paymentToken)).safeTransfer(KamiPlatform.platformAddress(), commissionAmount);
257
+ }
258
+ }
259
+
260
+ // Calculate remaining amount after platform commission
261
+ uint256 remainingAmount = tokenPrice - commissionAmount;
262
+
263
+ // Set token-specific mint royalties if provided
264
+ if(mintRoyalties.length > 0) {
265
+ KamiRoyalty.setTokenMintRoyalties(tokenId, mintRoyalties, KamiNFTCore.getExternalExistsReference(address(this)));
266
+ }
267
+
268
+ // Distribute mint royalties on the remaining amount
269
+ KamiRoyalty.distributeMintRoyalties(tokenId, remainingAmount, IERC20(address(paymentToken)));
270
+ }
271
+ }
272
+
273
+
274
+ /**
275
+ * @notice Set price for a specific token (only owner)
276
+ * @param tokenId Token ID
277
+ * @param newPrice New price in payment token
278
+ */
279
+ function setPrice(uint256 tokenId, uint256 newPrice) external onlyRole(OWNER_ROLE) {
280
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
281
+ tokenPrices[tokenId] = newPrice;
282
+ }
283
+
284
+ /**
285
+ * @notice Set URI for a specific token
286
+ * @param tokenId Token ID
287
+ * @param newTokenURI New URI for the token's metadata
288
+ */
289
+ function setTokenURI(uint256 tokenId, string calldata newTokenURI) external onlyRole(OWNER_ROLE) {
290
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
291
+ require(bytes(newTokenURI).length > 0, "Token URI cannot be empty");
292
+ tokenURIs[tokenId] = newTokenURI;
293
+ }
294
+
295
+ /**
296
+ * @notice Set base URI for token metadata
297
+ * @param newBaseTokenURI New base URI
298
+ */
299
+ function setBaseURI(string memory newBaseTokenURI) external onlyRole(OWNER_ROLE) {
300
+ _baseTokenURI = newBaseTokenURI;
301
+ }
302
+
303
+ /**
304
+ * @notice Returns the URI for a given token ID
305
+ * @dev Returns individual token URI if set, otherwise falls back to base URI
306
+ * @param tokenId The token ID to query
307
+ * @return The token URI
308
+ */
309
+ function tokenURI(uint256 tokenId) public view override returns (string memory) {
310
+ require(_ownerOf(tokenId) != address(0), "URI query for nonexistent token");
311
+
312
+ string memory individualURI = tokenURIs[tokenId];
313
+ if (bytes(individualURI).length > 0) {
314
+ return individualURI;
315
+ }
316
+
317
+ return string(abi.encodePacked(_baseTokenURI, Strings.toString(tokenId)));
318
+ }
319
+
320
+ /**
321
+ * @notice Get base URI
322
+ * @return Base URI string
323
+ */
324
+ function _baseURI() internal view override returns (string memory) {
325
+ return _baseTokenURI;
326
+ }
327
+
328
+ /**
329
+ * @notice Pause contract (only owner)
330
+ */
331
+ function pause() external onlyRole(OWNER_ROLE) {
332
+ _pause();
333
+ }
334
+
335
+ /**
336
+ * @notice Unpause contract (only owner)
337
+ */
338
+ function unpause() external onlyRole(OWNER_ROLE) {
339
+ _unpause();
340
+ }
341
+
342
+ /**
343
+ * @notice Set royalty percentage (only owner)
344
+ * @param percentage Royalty percentage in basis points (max 2000 = 20%)
345
+ */
346
+ function setRoyaltyPercentage(uint96 percentage) external onlyRole(OWNER_ROLE) {
347
+ KamiRoyalty.setRoyaltyPercentage(percentage);
348
+ }
349
+
350
+ /**
351
+ * @notice Get royalty percentage
352
+ * @return Royalty percentage in basis points
353
+ */
354
+ function royaltyPercentage() external view returns (uint96) {
355
+ return KamiRoyalty.royaltyPercentage();
356
+ }
357
+
358
+ /**
359
+ * @notice Get royalty info for a token
360
+ * @param tokenId The token ID
361
+ * @param price The sale price
362
+ * @return receiver The royalty receiver address
363
+ * @return royaltyAmount The royalty amount
364
+ */
365
+ function royaltyInfo(uint256 tokenId, uint256 price)
366
+ public
367
+ view
368
+ override
369
+ returns (address receiver, uint256 royaltyAmount)
370
+ {
371
+ uint256 totalRoyaltyAmount = (price * KamiRoyalty.royaltyPercentage()) / 10000;
372
+ KamiNFTCore.RoyaltyData[] memory royalties = KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
373
+ if (royalties.length > 0) {
374
+ KamiNFTCore.RoyaltyData memory info = royalties[0];
375
+ uint256 receiverShare = (totalRoyaltyAmount * info.feeNumerator) / 10000;
376
+ return (info.receiver, receiverShare);
377
+ }
378
+ return (address(0), 0);
379
+ }
380
+
381
+ /**
382
+ * @notice Get platform commission percentage
383
+ * @return Platform commission percentage in basis points
384
+ */
385
+ function platformCommission() external view returns (uint96) {
386
+ return KamiPlatform.platformCommission();
387
+ }
388
+
389
+ /**
390
+ * @notice Get platform address
391
+ * @return Platform address
392
+ */
393
+ function platformAddress() external view returns (address) {
394
+ return KamiPlatform.platformAddress();
395
+ }
396
+
397
+ /**
398
+ * @notice Set mint royalties
399
+ * @param royalties Array of royalty receivers and percentages
400
+ */
401
+ function setMintRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external onlyRole(OWNER_ROLE) {
402
+ KamiRoyalty.setMintRoyalties(royalties);
403
+ }
404
+
405
+ /**
406
+ * @notice Set transfer royalties
407
+ * @param royalties Array of royalty receivers and percentages
408
+ */
409
+ function setTransferRoyalties(KamiNFTCore.RoyaltyData[] calldata royalties) external onlyRole(OWNER_ROLE) {
410
+ KamiRoyalty.setTransferRoyalties(royalties);
411
+ }
412
+
413
+ /**
414
+ * @notice Get mint royalty receivers for a token
415
+ * @param tokenId Token ID
416
+ * @return Array of royalty receivers
417
+ */
418
+ function getMintRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
419
+ return KamiRoyalty.getMintRoyaltyReceivers(tokenId);
420
+ }
421
+
422
+ /**
423
+ * @notice Get transfer royalty receivers for a token
424
+ * @param tokenId Token ID
425
+ * @return Array of royalty receivers
426
+ */
427
+ function getTransferRoyaltyReceivers(uint256 tokenId) external view returns (KamiNFTCore.RoyaltyData[] memory) {
428
+ return KamiRoyalty.getTransferRoyaltyReceivers(tokenId);
429
+ }
430
+
431
+ /**
432
+ * @notice Update platform commission
433
+ * @param newPlatformCommissionPercentage New platform commission percentage
434
+ * @param newPlatformAddress New platform address
435
+ */
436
+ function setPlatformCommission(
437
+ uint96 newPlatformCommissionPercentage,
438
+ address newPlatformAddress
439
+ ) external onlyRole(OWNER_ROLE) {
440
+ KamiPlatform.updatePlatformCommission(
441
+ newPlatformCommissionPercentage,
442
+ newPlatformAddress,
443
+ address(this)
444
+ );
445
+ }
446
+
447
+ /**
448
+ * @notice Set token-specific mint royalties
449
+ * @param tokenId The token ID to set royalties for
450
+ * @param royalties Array of royalty receivers and percentages
451
+ */
452
+ function setTokenMintRoyalties(
453
+ uint256 tokenId,
454
+ KamiNFTCore.RoyaltyData[] calldata royalties
455
+ ) external onlyRole(OWNER_ROLE) {
456
+ KamiRoyalty.setTokenMintRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
457
+ }
458
+
459
+ /**
460
+ * @notice Set token-specific transfer royalties
461
+ * @param tokenId The token ID to set royalties for
462
+ * @param royalties Array of royalty receivers and percentages
463
+ */
464
+ function setTokenTransferRoyalties(
465
+ uint256 tokenId,
466
+ KamiNFTCore.RoyaltyData[] calldata royalties
467
+ ) external onlyRole(OWNER_ROLE) {
468
+ KamiRoyalty.setTokenTransferRoyalties(tokenId, royalties, KamiNFTCore.getExternalExistsReference(address(this)));
469
+ }
470
+
471
+
472
+ /**
473
+ * @notice Rent a token
474
+ * @param tokenId Token ID to rent
475
+ * @param duration Rental duration in seconds
476
+ * @param rentalPrice Total rental price
477
+ */
478
+ function rentToken(
479
+ uint256 tokenId,
480
+ uint256 duration,
481
+ uint256 rentalPrice,
482
+ address renter
483
+ ) external whenNotPaused {
484
+ require(exists(tokenId), "Token does not exist");
485
+ require(renter != address(0), "Renter cannot be zero address");
486
+ address rentalTokenOwner = ownerOf(tokenId);
487
+ require(rentalTokenOwner != renter, "Owner cannot rent their own token");
488
+
489
+ KamiRental.rentToken(
490
+ paymentToken,
491
+ tokenId,
492
+ duration,
493
+ rentalPrice,
494
+ rentalTokenOwner, // Pass the actual token owner to the library
495
+ msg.sender // payer
496
+ );
497
+
498
+ // Grant RENTER_ROLE to the renter
499
+ _grantRole(RENTER_ROLE, renter);
500
+ }
501
+
502
+ /**
503
+ * @notice End a rental early
504
+ * @param tokenId Token ID to end rental for
505
+ */
506
+ function endRental(uint256 tokenId) external {
507
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
508
+
509
+ KamiRental.endRentalSimple(tokenId);
510
+ }
511
+
512
+ /**
513
+ * @notice Extend a rental period
514
+ * @param tokenId Token ID to extend rental for
515
+ * @param additionalDuration Additional duration in seconds
516
+ * @param additionalPayment Additional payment required
517
+ */
518
+ function extendRental(
519
+ uint256 tokenId,
520
+ uint256 additionalDuration,
521
+ uint256 additionalPayment
522
+ ) external whenNotPaused {
523
+ require(exists(tokenId), "Token does not exist");
524
+ // The token owner for rental payment will be the address that holds the token
525
+ // In the context of this contract, `msg.sender` is implicitly the one who owns the token
526
+ // when initiating a rental, as per the previous discussion of ERC1155 ownership.
527
+ address rentalTokenOwner = ownerOf(tokenId);
528
+
529
+ KamiRental.extendRental(
530
+ paymentToken,
531
+ tokenId,
532
+ additionalDuration,
533
+ additionalPayment,
534
+ rentalTokenOwner, // The token owner is msg.sender here (Manually updated by user)
535
+ msg.sender // payer
536
+ );
537
+ }
538
+
539
+ /**
540
+ * @notice Get rental information for a token
541
+ * @param tokenId Token ID to get rental info for
542
+ * @return Rental information
543
+ */
544
+ function getRentalInfo(uint256 tokenId) external view returns (KamiNFTCore.Rental memory) {
545
+ return KamiRental.getRentalInfo(tokenId);
546
+ }
547
+
548
+ /**
549
+ * @notice Check if a token is currently rented
550
+ * @param tokenId Token ID to check
551
+ * @return True if the token is rented, false otherwise
552
+ */
553
+ function isRented(uint256 tokenId) external view returns (bool) {
554
+ return KamiRental.isRented(tokenId);
555
+ }
556
+
557
+ /**
558
+ * @notice Sell a token
559
+ * @param to The buyer address
560
+ * @param tokenId The token ID to sell
561
+ */
562
+ function sellToken(
563
+ address to,
564
+ uint256 tokenId,
565
+ address seller
566
+ ) external whenNotPaused {
567
+ require(exists(tokenId), "Token does not exist");
568
+ require(ownerOf(tokenId) == seller, "Seller is not token owner");
569
+ require(to != address(0), "Buyer cannot be zero address");
570
+ require(seller != address(0), "Seller cannot be zero address");
571
+
572
+ uint256 price = tokenPrices[tokenId];
573
+ require(price > 0, "Token price not set");
574
+
575
+ // Process sale with royalties FIRST (marks transfer as paid)
576
+ KamiTransfer.sellToken(paymentToken, tokenId, to, price, seller);
577
+
578
+ // Then transfer token (will now pass validation)
579
+ _transfer(seller, to, tokenId);
580
+ }
581
+
582
+ /**
583
+ * @notice Burn a token
584
+ * @param tokenId The token ID to burn
585
+ */
586
+ function burn(uint256 tokenId) external {
587
+ require(_ownerOf(tokenId) != address(0), "ERC721: token not minted");
588
+ require(ownerOf(tokenId) == msg.sender, "ERC721: caller is not token owner");
589
+
590
+ // Validate burn operation
591
+ KamiTransfer.validateBurn(tokenId, msg.sender);
592
+
593
+ _burn(tokenId);
594
+ }
595
+
596
+ /**
597
+ * @notice Initiate transfer with royalty requirement
598
+ * @param to The recipient address
599
+ * @param tokenId The token ID to transfer
600
+ * @param price The price
601
+ */
602
+ function initiateTransferWithRoyalty(
603
+ address to,
604
+ uint256 tokenId,
605
+ uint256 price
606
+ ) external {
607
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
608
+ require(ownerOf(tokenId) == msg.sender, "ERC721: caller is not token owner");
609
+
610
+ KamiTransfer.initiateTransferWithRoyalty(tokenId, to, price, msg.sender);
611
+ }
612
+
613
+ /**
614
+ * @notice Pay transfer royalty
615
+ * @param tokenId The token ID being transferred
616
+ * @param price The price
617
+ */
618
+ function payTransferRoyalty(
619
+ address seller,
620
+ uint256 tokenId,
621
+ uint256 price
622
+ ) external {
623
+ require(_ownerOf(tokenId) != address(0), "Token does not exist");
624
+
625
+ KamiTransfer.payTransferRoyalty(paymentToken, tokenId, price, seller, msg.sender);
626
+ }
627
+
628
+ /**
629
+ * @notice Check if transfer royalty is required
630
+ * @param tokenId The token ID being transferred
631
+ * @param price The price
632
+ * @return True if transfer royalty is required, false otherwise
633
+ */
634
+ function isTransferRoyaltyRequired(
635
+ address /* from */,
636
+ address /* to */,
637
+ uint256 tokenId,
638
+ uint256 price
639
+ ) external view returns (bool) {
640
+ return KamiTransfer.isTransferRoyaltyRequired(tokenId, price);
641
+ }
642
+
643
+ /**
644
+ * @notice Check if user has active rentals
645
+ * @return True if user has active rentals, false otherwise
646
+ */
647
+ function hasActiveRentals(address /* user */) internal view returns (bool) {
648
+ mapping(uint256 => KamiNFTCore.Rental) storage rentals = KamiRental._getRentals();
649
+ uint256 supply = _tokenIdCounter; // For ERC721, iterate up to current token ID
650
+ for (uint256 i = 1; i <= supply; i++) {
651
+ if (rentals[i].active && rentals[i].renter == msg.sender && block.timestamp < rentals[i].endTime) {
652
+ return true;
653
+ }
654
+ }
655
+ return false;
656
+ }
657
+
658
+ function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) {
659
+ address from = _ownerOf(tokenId);
660
+ address owner = super._update(to, tokenId, auth);
661
+
662
+ if (from != owner && from != address(0)) {
663
+ KamiNFTCore.Rental memory rental = KamiRental.getRentalInfo(tokenId);
664
+ if (rental.active) {
665
+ revert("ERC721: token is rented");
666
+ }
667
+ }
668
+
669
+ // Handle RENTER_ROLE revocation if a renter transfers their last rented token
670
+ if (from != owner && from != address(0) && to != address(0)) {
671
+ address renter = from;
672
+ if (hasRole(RENTER_ROLE, renter) && !KamiRental.isRented(tokenId) && !hasActiveRentals(renter)) {
673
+ _revokeRole(RENTER_ROLE, renter);
674
+ }
675
+ }
676
+
677
+ // Validate transfer if not minting or burning
678
+ if (from != owner && from != address(0) && to != address(0)) {
679
+ KamiTransfer.validateTransfer(
680
+ tokenId,
681
+ from,
682
+ to,
683
+ isApprovedForAll
684
+ );
685
+ // Update rental status on transfer
686
+ KamiTransfer.updateRentalOnTransferSimple(tokenId);
687
+ }
688
+
689
+ return owner;
690
+ }
691
+ }
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
5
+
6
+ /**
7
+ * @dev This is a ProxyAdmin used for managing proxies.
8
+ */
9
+ contract KAMIProxyAdmin is ProxyAdmin {
10
+ constructor(address owner) ProxyAdmin(owner) {
11
+ }
12
+ }