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