// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import "./interfaces/IVideoPayment.sol"; import "./libraries/VideoPaymentStorage.sol"; /** * @title VideoPayment * @dev Main contract for video content payment and access control */ contract VideoPayment is Initializable, UUPSUpgradeable, ReentrancyGuardUpgradeable, IVideoPayment { using VideoPaymentStorage for VideoPaymentStorage.VideoPaymentStorageStruct; // Storage VideoPaymentStorage.VideoPaymentStorageStruct private _storage; // Modifiers modifier onlyOwner() { if (msg.sender != _storage.owner) revert NotOwner(); _; } modifier onlyAdmin() { if (!_storage.isAdmin[msg.sender]) revert NotAdmin(); _; } modifier whenNotPaused() { if (_storage.paused) revert ContractPaused(); _; } modifier validContent(uint256 contentId) { if (!_storage.contentConfigs[contentId].isActive) revert InvalidContent(); _; } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /** * @dev Initialize the contract * @param initialOwner Initial owner address * @param initialAdmins Initial admin addresses array * @param supportedTokenAddresses Initial supported token addresses array */ function initialize( address initialOwner, address[] memory initialAdmins, address[] memory supportedTokenAddresses ) public initializer { if (initialOwner == address(0)) revert ZeroAddress(); __UUPSUpgradeable_init(); __ReentrancyGuard_init(); _storage.owner = initialOwner; _storage.version = 1; _storage.paused = false; // Add initial admins for (uint256 i = 0; i < initialAdmins.length; i++) { if (initialAdmins[i] != address(0)) { _storage.isAdmin[initialAdmins[i]] = true; emit AdminAdded(initialAdmins[i]); } } // Add initial supported tokens for (uint256 i = 0; i < supportedTokenAddresses.length; i++) { if (supportedTokenAddresses[i] != address(0)) { _storage.supportedTokens[supportedTokenAddresses[i]] = true; emit SupportedTokenAdded(supportedTokenAddresses[i]); } } } /** * @dev Get contract version */ function version() external view returns (uint256) { return _storage.version; } /** * @dev Migration function for future upgrades */ function migrate() external onlyOwner { // This function will be implemented in future versions if needed // Migration logic can be added here for data structure updates // No event emission needed as this is not an upgrade operation } /** * @dev Authorize upgrade (required by UUPSUpgradeable) */ function _authorizeUpgrade( address newImplementation ) internal override onlyOwner {} // ============ Owner Management Functions ============ /** * @dev Add admin * @param admin Admin address to add */ function addAdmin(address admin) external onlyOwner { if (admin == address(0)) revert ZeroAddress(); _storage.isAdmin[admin] = true; emit AdminAdded(admin); } /** * @dev Remove admin * @param admin Admin address to remove */ function removeAdmin(address admin) external onlyOwner { if (admin == address(0)) revert ZeroAddress(); _storage.isAdmin[admin] = false; emit AdminRemoved(admin); } // ============ Content Management Functions ============ /** * @dev Set content configuration * @param contentId Content ID * @param nativePrice Native coin price * @param defaultViewCount Default view count * @param viewDuration View duration in seconds * @param isActive Whether content is active */ function setContentConfig( uint256 contentId, uint256 nativePrice, uint256 defaultViewCount, uint256 viewDuration, bool isActive ) external onlyAdmin { _storage.contentConfigs[contentId].nativePrice = nativePrice; _storage.contentConfigs[contentId].defaultViewCount = defaultViewCount; _storage.contentConfigs[contentId].viewDuration = viewDuration; _storage.contentConfigs[contentId].isActive = isActive; emit ContentConfigUpdated(contentId); } /** * @dev Batch set content configurations * @param contentIds Content ID array * @param nativePrices Native price array * @param defaultViewCounts Default view count array * @param viewDurations View duration array * @param isActiveArray Active status array */ function batchSetContentConfig( uint256[] memory contentIds, uint256[] memory nativePrices, uint256[] memory defaultViewCounts, uint256[] memory viewDurations, bool[] memory isActiveArray ) external onlyAdmin { if ( contentIds.length != nativePrices.length || contentIds.length != defaultViewCounts.length || contentIds.length != viewDurations.length || contentIds.length != isActiveArray.length ) revert ArrayLengthMismatch(); for (uint256 i = 0; i < contentIds.length; i++) { _storage.contentConfigs[contentIds[i]].nativePrice = nativePrices[ i ]; _storage .contentConfigs[contentIds[i]] .defaultViewCount = defaultViewCounts[i]; _storage.contentConfigs[contentIds[i]].viewDuration = viewDurations[ i ]; _storage.contentConfigs[contentIds[i]].isActive = isActiveArray[i]; emit ContentConfigUpdated(contentIds[i]); } } /** * @dev Check if content is active * @param contentId Content ID * @return Whether content is active */ function isContentActive(uint256 contentId) external view returns (bool) { return _storage.contentConfigs[contentId].isActive; } // ============ Price Management Functions ============ /** * @dev Update token price for content (address(0) for native token) * @param contentId Content ID * @param token Token address (address(0) for native) * @param price New price */ function updateTokenPrice( uint256 contentId, address token, uint256 price ) external onlyAdmin { if (!_storage.supportedTokens[token]) revert UnsupportedToken(); _storage.contentConfigs[contentId].tokenPrices[token] = price; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentId].nativePrice = price; } emit TokenPriceUpdated(contentId, token, price); } /** * @dev Batch update token prices for multiple contents (address(0) for native token) * @param contentIds Content ID array * @param token Token address (address(0) for native) * @param prices Price array */ function batchUpdateTokenPrice( uint256[] memory contentIds, address token, uint256[] memory prices ) external onlyAdmin { if (contentIds.length != prices.length) revert ArrayLengthMismatch(); if (!_storage.supportedTokens[token]) revert UnsupportedToken(); for (uint256 i = 0; i < contentIds.length; i++) { _storage.contentConfigs[contentIds[i]].tokenPrices[token] = prices[ i ]; // Sync with nativePrice for backward compatibility if (token == address(0)) { _storage.contentConfigs[contentIds[i]].nativePrice = prices[i]; } emit TokenPriceUpdated(contentIds[i], token, prices[i]); } } // ============ Payment Token Management Functions ============ /** * @dev Add supported token (address(0) for native token) * @param token Token address to add (address(0) for native) */ function addSupportedToken(address token) external onlyAdmin { _storage.supportedTokens[token] = true; emit SupportedTokenAdded(token); } /** * @dev Remove supported token (address(0) for native token) * @param token Token address to remove (address(0) for native) */ function removeSupportedToken(address token) external onlyAdmin { _storage.supportedTokens[token] = false; emit SupportedTokenRemoved(token); } /** * @dev Check if token is supported * @param token Token address to check * @return Whether token is supported */ function isSupportedToken(address token) external view returns (bool) { return _storage.supportedTokens[token]; } // ============ Purchase Functions ============ /** * @dev Purchase content with token (address(0) for native token) * @param contentId Content ID * @param paymentToken Payment token address (address(0) for native) * @param amount Payment amount */ function purchaseContent( uint256 contentId, address paymentToken, uint256 amount ) external payable nonReentrant whenNotPaused validContent(contentId) { if (!_storage.supportedTokens[paymentToken]) revert UnsupportedToken(); uint256 expectedPrice; if (paymentToken == address(0)) { // Native token payment expectedPrice = _storage.contentConfigs[contentId].nativePrice; if (msg.value != expectedPrice) revert InsufficientPayment(); if (amount != msg.value) revert InsufficientPayment(); } else { // ERC20 token payment expectedPrice = _storage.contentConfigs[contentId].tokenPrices[ paymentToken ]; if (expectedPrice != amount) revert InsufficientPayment(); IERC20(paymentToken).transferFrom( msg.sender, address(this), amount ); } // Create view permission _updateViewPermission(msg.sender, contentId); // Emit event uint256 validUntil = block.timestamp + _storage.contentConfigs[contentId].viewDuration; emit ContentPurchased( msg.sender, contentId, paymentToken, amount, _storage.contentConfigs[contentId].defaultViewCount, validUntil ); } /** * @dev Batch purchase content with token (address(0) for native token) * @param contentIds Content ID array * @param paymentToken Payment token address (address(0) for native) * @param totalAmount Total payment amount */ function batchPurchase( uint256[] memory contentIds, address paymentToken, uint256 totalAmount ) external payable nonReentrant whenNotPaused { if (!_storage.supportedTokens[paymentToken]) revert UnsupportedToken(); uint256 expectedTotal = 0; for (uint256 i = 0; i < contentIds.length; i++) { if (!_storage.contentConfigs[contentIds[i]].isActive) revert InvalidContent(); if (paymentToken == address(0)) { expectedTotal += _storage .contentConfigs[contentIds[i]] .nativePrice; } else { expectedTotal += _storage .contentConfigs[contentIds[i]] .tokenPrices[paymentToken]; } } if (totalAmount != expectedTotal) revert InsufficientPayment(); if (paymentToken == address(0)) { // Native token payment if (msg.value != totalAmount) revert InsufficientPayment(); } else { // ERC20 token payment IERC20(paymentToken).transferFrom( msg.sender, address(this), totalAmount ); } // Create view permissions for (uint256 i = 0; i < contentIds.length; i++) { _updateViewPermission(msg.sender, contentIds[i]); } // Emit event emit BatchPurchased(msg.sender, contentIds, paymentToken, totalAmount); } /** * @dev Purchase content with NFT * @param contentId Content ID * @param nftContract NFT contract address * @param tokenId NFT token ID */ function purchaseWithNFT( uint256 contentId, address nftContract, uint256 tokenId ) external nonReentrant whenNotPaused validContent(contentId) { // Check NFT ownership if (IERC721(nftContract).ownerOf(tokenId) != msg.sender) revert NoValidPermission(); // Transfer NFT (burn it by transferring to this contract) IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId); // Create view permission _updateViewPermission(msg.sender, contentId); // Emit event emit NFTPurchased(msg.sender, contentId, nftContract, tokenId); } /** * @dev Internal function to update view permission * @param user User address * @param contentId Content ID */ function _updateViewPermission(address user, uint256 contentId) internal { VideoPaymentStorage.ViewPermission storage existing = _storage .userPermissions[user][contentId]; uint256 defaultViews = _storage .contentConfigs[contentId] .defaultViewCount; uint256 duration = _storage.contentConfigs[contentId].viewDuration; if (defaultViews == 0) { // One-time purchase: Check if already purchased and still valid if ( existing.isValid && block.timestamp <= existing.purchaseTime + duration ) { // User already has valid permission, no need to do anything // This allows the purchase to succeed without duplicate payment return; } // Create new permission for unlimited viewing or refresh expired permission existing.purchaseTime = block.timestamp; existing.remainingViews = 0; // 0 means unlimited existing.isValid = true; } else { // View-count based: Add to existing or create new if ( existing.isValid && block.timestamp <= existing.purchaseTime + duration ) { // Existing permission is still valid, accumulate view count existing.remainingViews += defaultViews; emit ViewCountAdded( user, contentId, defaultViews, existing.remainingViews ); } else { // No existing permission or expired, create new existing.purchaseTime = block.timestamp; existing.remainingViews = defaultViews; existing.isValid = true; } } } // ============ Permission Verification Functions ============ /** * @dev Check if user has valid view permission * @param user User address * @param contentId Content ID * @return Whether user has valid permission */ function hasViewPermission( address user, uint256 contentId ) external view returns (bool) { VideoPaymentStorage.ViewPermission memory permission = _storage .userPermissions[user][contentId]; if (!permission.isValid) { return false; } // Check if permission has expired if ( block.timestamp > permission.purchaseTime + _storage.contentConfigs[contentId].viewDuration ) { return false; } // Get default view count to determine content type uint256 defaultViews = _storage .contentConfigs[contentId] .defaultViewCount; if (defaultViews == 0) { // One-time purchase: valid if not expired (remainingViews should be 0) return true; } else { // View-count based: valid if there are remaining views return permission.remainingViews > 0; } } /** * @dev Get user permissions for multiple contents * @param user User address * @param contentIds Content ID array * @return Permission array */ function getUserPermissions( address user, uint256[] memory contentIds ) external view returns (VideoPaymentStorage.ViewPermission[] memory) { VideoPaymentStorage.ViewPermission[] memory permissions = new VideoPaymentStorage.ViewPermission[]( contentIds.length ); for (uint256 i = 0; i < contentIds.length; i++) { permissions[i] = _storage.userPermissions[user][contentIds[i]]; } return permissions; } /** * @dev Get detailed permission info * @param user User address * @param contentId Content ID * @return Permission details */ function getPermissionDetails( address user, uint256 contentId ) external view returns (VideoPaymentStorage.ViewPermission memory) { return _storage.userPermissions[user][contentId]; } /** * @dev Consume one view for user * @param user User address * @param contentId Content ID * @return Success status */ function consumeView( address user, uint256 contentId ) external onlyAdmin returns (bool) { VideoPaymentStorage.ViewPermission storage permission = _storage .userPermissions[user][contentId]; if (!permission.isValid) { revert NoValidPermission(); } // Check if permission has expired if ( block.timestamp > permission.purchaseTime + _storage.contentConfigs[contentId].viewDuration ) { revert NoValidPermission(); } // Get default view count to determine if this is unlimited or count-based uint256 defaultViews = _storage .contentConfigs[contentId] .defaultViewCount; if (defaultViews == 0) { // One-time purchase with unlimited viewing - don't decrement emit ViewConsumed(user, contentId, 0); // 0 indicates unlimited return true; } else { // View-count based purchase - check and decrement if (permission.remainingViews == 0) { revert NoValidPermission(); } // Consume one view permission.remainingViews--; emit ViewConsumed(user, contentId, permission.remainingViews); // Mark as invalid if no views remaining if (permission.remainingViews == 0) { permission.isValid = false; emit PermissionExpired(user, contentId); } return true; } } /** * @dev Batch consume views for multiple users * @param users User address array * @param contentIds Content ID array * @return Success status array */ function batchConsumeView( address[] memory users, uint256[] memory contentIds ) external onlyAdmin returns (bool[] memory) { if (users.length != contentIds.length) revert ArrayLengthMismatch(); bool[] memory results = new bool[](users.length); for (uint256 i = 0; i < users.length; i++) { VideoPaymentStorage.ViewPermission storage permission = _storage .userPermissions[users[i]][contentIds[i]]; if (!permission.isValid) { results[i] = false; continue; } // Check if permission has expired if ( block.timestamp > permission.purchaseTime + _storage.contentConfigs[contentIds[i]].viewDuration ) { results[i] = false; continue; } // Get default view count to determine if this is unlimited or count-based uint256 defaultViews = _storage .contentConfigs[contentIds[i]] .defaultViewCount; if (defaultViews == 0) { // One-time purchase with unlimited viewing - don't decrement emit ViewConsumed(users[i], contentIds[i], 0); // 0 indicates unlimited results[i] = true; } else { // View-count based purchase - check and decrement if (permission.remainingViews == 0) { results[i] = false; continue; } // Consume one view permission.remainingViews--; emit ViewConsumed( users[i], contentIds[i], permission.remainingViews ); // Mark as invalid if no views remaining if (permission.remainingViews == 0) { permission.isValid = false; emit PermissionExpired(users[i], contentIds[i]); } results[i] = true; } } return results; } // ============ Content Query Functions ============ /** * @dev Get content purchase information * @param contentId Content ID * @return Purchase information including supported tokens and prices */ function getContentPurchaseInfo( uint256 contentId ) external view returns (IVideoPayment.ContentPurchaseInfo memory) { VideoPaymentStorage.ContentConfig storage config = _storage .contentConfigs[contentId]; // Get supported tokens for this content ( address[] memory supportedTokens, uint256[] memory prices ) = _getSupportedTokensForContent(contentId); return IVideoPayment.ContentPurchaseInfo({ defaultViewCount: config.defaultViewCount, viewDuration: config.viewDuration, isActive: config.isActive, supportedTokens: supportedTokens, tokenPrices: prices, isUnlimitedViewing: config.defaultViewCount == 0 }); } /** * @dev Internal function to get supported tokens and prices for content * @param contentId Content ID * @return supportedTokens Array of supported token addresses * @return prices Array of corresponding prices */ function _getSupportedTokensForContent( uint256 contentId ) internal view returns (address[] memory supportedTokens, uint256[] memory prices) { // Count supported tokens uint256 count = 0; // Check native token if ( _storage.supportedTokens[address(0)] && _storage.contentConfigs[contentId].nativePrice > 0 ) { count++; } // This is a simplified approach - in practice, you'd need to iterate through known tokens // For now, we'll create arrays for known supported tokens from the contract state // This requires tracking supported tokens in a more efficient way supportedTokens = new address[](count); prices = new uint256[](count); uint256 index = 0; // Add native token if supported and has price if ( _storage.supportedTokens[address(0)] && _storage.contentConfigs[contentId].nativePrice > 0 ) { supportedTokens[index] = address(0); prices[index] = _storage.contentConfigs[contentId].nativePrice; index++; } // Note: For ERC20 tokens, we'd need to iterate through known tokens // This implementation is simplified for the upgrade } // ============ Emergency Control Functions ============ /** * @dev Pause contract */ function pause() external onlyOwner { _storage.paused = true; emit Paused(); } /** * @dev Unpause contract */ function unpause() external onlyOwner { _storage.paused = false; emit Unpaused(); } }