@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.
- package/dist/KAMI-NFTs/KAMI1155CUpgradeable.sol +1069 -0
- package/dist/KAMI-NFTs/KAMI721ACUpgradable.sol +949 -0
- package/dist/KAMI-NFTs/KAMI721CUpgradeable.sol +691 -0
- package/dist/KAMI-NFTs/ProxyAdmin.sol +12 -0
- package/dist/KAMI-NFTs/TransparentUpgradeableProxy.sol +15 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI1155CUpgradeable.sol/KAMI1155CUpgradeable.dbg.json +4 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI1155CUpgradeable.sol/KAMI1155CUpgradeable.json +2207 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI721ACUpgradable.sol/KAMI721ACUpgradable.dbg.json +4 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI721ACUpgradable.sol/KAMI721ACUpgradable.json +2210 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI721CUpgradeable.sol/KAMI721CUpgradeable.dbg.json +4 -0
- package/dist/KAMI-NFTs/artifacts/contracts/KAMI721CUpgradeable.sol/KAMI721CUpgradeable.json +1823 -0
- package/dist/KAMI-NFTs/artifacts/contracts/ProxyAdmin.sol/KAMIProxyAdmin.dbg.json +4 -0
- package/dist/KAMI-NFTs/artifacts/contracts/ProxyAdmin.sol/KAMIProxyAdmin.json +132 -0
- package/dist/KAMI-NFTs/artifacts/contracts/TransparentUpgradeableProxy.sol/KAMITransparentUpgradeableProxy.dbg.json +4 -0
- package/dist/KAMI-NFTs/artifacts/contracts/TransparentUpgradeableProxy.sol/KAMITransparentUpgradeableProxy.json +116 -0
- package/dist/kami-upgrade-manager.d.ts +65 -0
- package/dist/kami-upgrade-manager.d.ts.map +1 -0
- package/dist/kami-upgrade-manager.js +320 -0
- package/dist/kami-upgrade-manager.js.map +1 -0
- 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
|
+
}
|