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