Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"contracts/EternalStorage.sol": {
"content": "// SPDX-License-Identifier: MIT\r
pragma solidity 0.8.24;\r
\r
import "@openzeppelin/contracts/access/Ownable.sol";\r
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";\r
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";\r
\r
contract PermanStorageOne is Ownable, ReentrancyGuard {\r
// ============= Storage Variables ============\r
string public name;\r
string public symbol;\r
uint256 public feeQty;\r
uint256 public limitMediaId;\r
\r
struct Media {\r
address owner;\r
string description;\r
uint256 mediaType; // 0 = video, 1 = image\r
uint256 timestamp;\r
bytes[] chunks;\r
bool finalized;\r
uint256 totalChunks;\r
uint256 chunksUploaded;\r
}\r
\r
IERC20 public chainGalleryToken;\r
\r
// ============= Sale Variables ============\r
uint256 public askingPrice;\r
address public payoutAddress;\r
uint256 public constant MAX_BUYERS = 7;\r
\r
bool public saleActive;\r
bool public allowBidWithdrawal;\r
uint256 public emergencyWithdrawTimestamp;\r
uint256 public constant EMERGENCY_DELAY = 7 days;\r
\r
struct Buyer {\r
address buyerAddress;\r
uint256 bidAmount;\r
bool exists;\r
}\r
\r
Buyer[] public buyers;\r
mapping(address => uint256) public buyerIndex;\r
\r
struct FailedRefund {\r
address buyer;\r
uint256 amount;\r
uint256 timestamp;\r
bool claimed;\r
}\r
\r
mapping(address => FailedRefund) public failedRefunds;\r
address[] public failedRefundAddresses;\r
\r
// ============= Fee Variables ============\r
address public feeReceiver;\r
uint256 public withdrawalFee = 0.0001 ether;\r
\r
// ============= Subscription Variables ============\r
struct Subscription {\r
address subscriber;\r
uint256 startTime;\r
uint256 endTime;\r
uint256 mediaId;\r
bool active;\r
}\r
\r
struct SubscriptionPlan {\r
uint256 duration;\r
uint256 price;\r
bool active;\r
}\r
\r
mapping(address => mapping(uint256 => Subscription)) public subscriptions;\r
mapping(uint256 => SubscriptionPlan) public subscriptionPlans;\r
uint256 public nextPlanId;\r
uint256 public subFee;\r
mapping(address => bool) public authorizedReaders;\r
\r
// ============= Media Storage ============\r
mapping(uint256 => Media) public media;\r
mapping(address => uint256[]) public userMedia;\r
uint256[] public allMedia;\r
uint256 public nextMediaId;\r
\r
// ============= Events ============\r
// Media Events\r
event MediaCreated(uint256 indexed mediaId, address indexed owner, uint256 mediaType, string description);\r
event ChunkAdded(uint256 indexed mediaId, uint256 chunkIndex, uint256 chunkSize);\r
event MediaFinalized(uint256 indexed mediaId);\r
event MediaDefinalized(uint256 indexed mediaId);\r
event MediaDescriptionUpdated(uint256 indexed mediaId, string newDescription);\r
event sendTokensToContract_(address indexed to, uint256 amount);\r
\r
// Sale Events\r
event SaleStarted(uint256 price, address payoutTo);\r
event SaleStopped();\r
event BidPlaced(address indexed buyer, uint256 amount);\r
event SaleCompleted(address indexed buyer, uint256 price);\r
event BidAccepted(address indexed buyer, uint256 acceptedPrice);\r
event WithdrawalPermissionChanged(bool allowed);\r
event BidWithdrawn(address indexed buyer, uint256 refundAmount);\r
event EmergencyWithdrawInitiated(uint256 timestamp, uint256 unlockTime);\r
event EmergencyWithdrawExecuted(uint256 amount, uint256 timestamp);\r
\r
// Fee Events\r
event WithdrawalFeeUpdated(uint256 newFee);\r
event TokensWithdrawn(address indexed to, uint256 amount, uint256 ethFee);\r
\r
// Refund Events\r
event RefundFailed(address indexed buyer, uint256 amount, uint256 timestamp);\r
event RefundClaimed(address indexed buyer, uint256 amount);\r
event ManualRefundProcessed(address indexed buyer, uint256 amount);\r
\r
// Subscription Events\r
event SubscriptionPlanCreated(uint256 indexed planId, uint256 duration, uint256 price);\r
event SubscriptionPlanUpdated(uint256 indexed planId, uint256 duration, uint256 price, bool active);\r
event Subscribed(address indexed subscriber, uint256 indexed mediaId, uint256 planId, uint256 endTime);\r
event AuthorizedReaderUpdated(address indexed reader, bool authorized);\r
\r
// ============= Constructor ============\r
constructor(uint256 id, uint256 _feeQty, address feeReceiver_, address owner_,address chainGalleryToken_ ) Ownable(owner_) {\r
require(id > 0, "Invalid media ID");\r
subFee = 0;\r
limitMediaId=5;\r
\r
// Updated to ChainGallery token\r
chainGalleryToken = IERC20(address(chainGalleryToken_));\r
\r
string memory paddedId = _padId(id, 13);\r
name = string(abi.encodePacked("EternalStorage_", paddedId));\r
symbol = string(abi.encodePacked("EStoOne_", paddedId));\r
\r
saleActive = false;\r
feeQty = _feeQty;\r
feeReceiver = feeReceiver_;\r
allowBidWithdrawal = false;\r
nextGroupId = 0;\r
emergencyWithdrawTimestamp = 0;\r
_transferOwnership(owner_);\r
}\r
\r
// ============= Modifiers ============\r
modifier hasRequiredChainGalleryBalance(uint256 monthNumber) {\r
require(monthNumber >= 1 && monthNumber <= 12, "Invalid month");\r
uint256 requiredBalance = monthNumber * feeQty * (10 ** 18);\r
require(chainGalleryToken.balanceOf(address(this)) >= requiredBalance, "Contract insufficient ChainGallery");\r
_;\r
}\r
\r
\r
// ============= Internal Helper Functions ============\r
function _uintToString(uint256 value) internal pure returns (string memory) {\r
if (value == 0) return "0";\r
uint256 temp = value;\r
uint256 digits;\r
while (temp != 0) {\r
digits++;\r
temp /= 10;\r
}\r
bytes memory buffer = new bytes(digits);\r
while (value != 0) {\r
digits -= 1;\r
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));\r
value /= 10;\r
}\r
return string(buffer);\r
}\r
\r
function _padId(uint256 id, uint256 length) internal pure returns (string memory) {\r
string memory idStr = _uintToString(id);\r
uint256 idLen = bytes(idStr).length;\r
\r
if (idLen >= length) {\r
return idStr;\r
}\r
\r
bytes memory padded = new bytes(length);\r
uint256 padLen = length - idLen;\r
\r
for (uint256 i = 0; i < padLen; i++) {\r
padded[i] = bytes1("0");\r
}\r
\r
for (uint256 i = 0; i < idLen; i++) {\r
padded[padLen + i] = bytes(idStr)[i];\r
}\r
\r
return string(padded);\r
}\r
\r
function _findEmptySlot() internal view returns (uint256) {\r
for (uint256 i = 0; i < buyers.length; i++) {\r
if (!buyers[i].exists) {\r
return i;\r
}\r
}\r
return type(uint256).max;\r
}\r
\r
function _getActiveBuyersCount() internal view returns (uint256) {\r
uint256 count = 0;\r
for (uint256 i = 0; i < buyers.length; i++) {\r
if (buyers[i].exists) {\r
count++;\r
}\r
}\r
return count;\r
}\r
\r
function _clearBuyersData() internal {\r
for (uint256 i = 0; i < buyers.length; i++) {\r
if (buyers[i].exists) {\r
delete buyerIndex[buyers[i].buyerAddress];\r
}\r
}\r
delete buyers;\r
}\r
\r
function _recordFailedRefund(address buyer, uint256 amount) internal {\r
if (failedRefunds[buyer].amount == 0) {\r
failedRefundAddresses.push(buyer);\r
}\r
\r
failedRefunds[buyer] = FailedRefund({\r
buyer: buyer,\r
amount: amount,\r
timestamp: block.timestamp,\r
claimed: false\r
});\r
}\r
\r
function _hasActiveSubscription(address subscriber, uint256 mediaId) internal view returns (bool) {\r
Subscription storage sub = subscriptions[subscriber][mediaId];\r
return sub.active && sub.endTime > block.timestamp;\r
}\r
\r
function _refundAllBuyersExcept(address exceptBuyer) internal {\r
Buyer[] memory buyersToRefund = buyers;\r
\r
for (uint256 i = 0; i < buyersToRefund.length; i++) {\r
if (buyersToRefund[i].exists && buyersToRefund[i].buyerAddress != exceptBuyer) {\r
address buyerAddr = buyersToRefund[i].buyerAddress;\r
uint256 refundAmount = buyersToRefund[i].bidAmount;\r
\r
if (refundAmount > 0) {\r
(bool sent, ) = payable(buyerAddr).call{value: refundAmount}("");\r
if (!sent) {\r
_recordFailedRefund(buyerAddr, refundAmount);\r
emit RefundFailed(buyerAddr, refundAmount, block.timestamp);\r
}\r
}\r
}\r
}\r
}\r
\r
function _transferOwnershipWithSale(address newOwner, uint256 salePrice) internal nonReentrant {\r
require(newOwner != address(0), "New owner is the zero address");\r
require(saleActive, "Sale is not active");\r
\r
saleActive = false;\r
_transferOwnership(newOwner);\r
\r
// Process payments\r
uint256 feeAmount = (salePrice * 2) / 100;\r
uint256 payoutAmount = salePrice - feeAmount;\r
\r
// Transfer funds\r
(bool success, ) = payable(payoutAddress).call{value: payoutAmount}("");\r
require(success, "Payout failed");\r
\r
(bool feeSuccess, ) = payable(feeReceiver).call{value: feeAmount}("");\r
require(feeSuccess, "Fee transfer failed");\r
\r
// Refund other buyers\r
_refundAllBuyersExcept(newOwner);\r
_clearBuyersData();\r
\r
emit SaleCompleted(newOwner, salePrice);\r
}\r
function hasPlacedBid(address user) external view returns (bool) {\r
uint256 index = buyerIndex[user];\r
return index > 0 && index <= buyers.length && buyers[index - 1].exists;\r
}\r
// ============= Media Functions ============\r
function createMedia(\r
string memory description,\r
uint256 mediaType,\r
uint256 totalChunks,\r
uint256 monthNumber\r
) public onlyOwner hasRequiredChainGalleryBalance(monthNumber) returns (uint256) {\r
require(mediaType == 0 || mediaType == 1, "Invalid media type");\r
require(nextMediaId <=limitMediaId, "limitMediaId");\r
require(totalChunks > 0, "Total chunks must be greater than 0");\r
limitMediaId=monthNumber*5;\r
uint256 mediaId = nextMediaId++;\r
Media storage newMedia = media[mediaId];\r
newMedia.owner = msg.sender;\r
newMedia.description = description;\r
newMedia.mediaType = mediaType;\r
newMedia.timestamp = block.timestamp;\r
newMedia.totalChunks = totalChunks;\r
newMedia.chunksUploaded = 0;\r
\r
for (uint256 i = 0; i < totalChunks; i++) {\r
newMedia.chunks.push(bytes(""));\r
}\r
\r
userMedia[msg.sender].push(mediaId);\r
allMedia.push(mediaId);\r
\r
emit MediaCreated(mediaId, msg.sender, mediaType, description);\r
return mediaId;\r
}\r
\r
function addChunk(\r
uint256 mediaId,\r
uint256 chunkIndex,\r
bytes memory chunkData\r
) public onlyOwner {\r
Media storage mediaItem = media[mediaId];\r
require(mediaItem.owner == msg.sender, "Not the owner");\r
require(!mediaItem.finalized, "Media is finalized");\r
require(chunkIndex < mediaItem.totalChunks, "Invalid chunk index");\r
require(mediaItem.chunks[chunkIndex].length == 0, "Chunk already uploaded");\r
\r
mediaItem.chunks[chunkIndex] = chunkData;\r
mediaItem.chunksUploaded++;\r
\r
emit ChunkAdded(mediaId, chunkIndex, chunkData.length);\r
}\r
\r
function addChunksBatch(\r
uint256 mediaId,\r
uint256[] memory chunkIndices,\r
bytes[] memory chunksData\r
) public onlyOwner {\r
require(chunkIndices.length == chunksData.length, "Arrays length mismatch");\r
\r
\r
for (uint256 i = 0; i < chunkIndices.length; i++) {\r
addChunk(mediaId, chunkIndices[i], chunksData[i]);\r
}\r
}\r
\r
function storeMedia(\r
string memory description,\r
uint256 mediaType,\r
bytes[] memory chunks,\r
uint256 monthNumber\r
) public onlyOwner hasRequiredChainGalleryBalance(monthNumber) returns (uint256) {\r
require(chunks.length > 0, "No chunks provided");\r
\r
uint256 mediaId = createMedia(description, mediaType, chunks.length, monthNumber);\r
\r
for (uint256 i = 0; i < chunks.length; i++) {\r
media[mediaId].chunks[i] = chunks[i];\r
}\r
media[mediaId].chunksUploaded = chunks.length;\r
\r
return mediaId;\r
}\r
\r
function finalizeMedia(uint256 mediaId) public onlyOwner {\r
Media storage mediaItem = media[mediaId];\r
require(mediaItem.owner == msg.sender, "Not the owner");\r
require(!mediaItem.finalized, "Already finalized");\r
require(mediaItem.chunksUploaded == mediaItem.totalChunks, "Not all chunks uploaded");\r
\r
mediaItem.finalized = true;\r
emit MediaFinalized(mediaId);\r
}\r
\r
\r
// ============= Media Access Functions (Fixed Access Control) ============\r
function getMedia(uint256 mediaId) public view canAccessMedia(mediaId) returns (\r
address owner,\r
string memory description,\r
uint256 mediaType,\r
uint256 timestamp,\r
bytes[] memory chunks,\r
uint256 totalChunks,\r
uint256 chunksUploaded,\r
bool finalized\r
) {\r
Media storage mediaItem = media[mediaId];\r
return (\r
mediaItem.owner,\r
mediaItem.description,\r
mediaItem.mediaType,\r
mediaItem.timestamp,\r
mediaItem.chunks,\r
mediaItem.totalChunks,\r
mediaItem.chunksUploaded,\r
mediaItem.finalized\r
);\r
}\r
\r
function getMediaChunk(uint256 mediaId, uint256 chunkIndex) public view canAccessMedia(mediaId) returns (bytes memory) {\r
Media storage mediaItem = media[mediaId];\r
require(chunkIndex < mediaItem.chunks.length, "Invalid chunk index");\r
return mediaItem.chunks[chunkIndex];\r
}\r
\r
function getMediaProgress(uint256 mediaId) public view canAccessMedia(mediaId) returns (uint256 uploaded, uint256 total) {\r
Media storage mediaItem = media[mediaId];\r
return (mediaItem.chunksUploaded, mediaItem.totalChunks);\r
}\r
\r
function getUserMedia(address user) public view returns (uint256[] memory) {\r
// Allow anyone to see which media a user owns (public information)\r
return userMedia[user];\r
}\r
\r
function getAllMedia() public view returns (uint256[] memory) {\r
// Allow anyone to see all media IDs (public information)\r
return allMedia;\r
}\r
\r
function getMediaCount() public view returns (uint256) {\r
return allMedia.length;\r
}\r
\r
// ============= Sale Functions ============\r
function startSale(uint256 _askingPrice, address _payoutAddress) external onlyOwner {\r
require(_askingPrice > 0, "Price must be greater than 0");\r
require(_payoutAddress != address(0), "Invalid payout address");\r
require(!saleActive, "Sale already active");\r
\r
_clearBuyersData();\r
\r
askingPrice = _askingPrice;\r
payoutAddress = _payoutAddress;\r
saleActive = true;\r
allowBidWithdrawal = false;\r
\r
emit SaleStarted(_askingPrice, _payoutAddress);\r
}\r
\r
function stopSale() external onlyOwner nonReentrant {\r
require(saleActive, "Sale is not active");\r
\r
saleActive = false;\r
Buyer[] memory buyersToRefund = buyers;\r
_clearBuyersData();\r
\r
for (uint256 i = 0; i < buyersToRefund.length; i++) {\r
if (buyersToRefund[i].exists && buyersToRefund[i].bidAmount > 0) {\r
(bool sent, ) = payable(buyersToRefund[i].buyerAddress).call{value: buyersToRefund[i].bidAmount}("");\r
if (!sent) {\r
_recordFailedRefund(buyersToRefund[i].buyerAddress, buyersToRefund[i].bidAmount);\r
emit RefundFailed(buyersToRefund[i].buyerAddress, buyersToRefund[i].bidAmount, block.timestamp);\r
}\r
}\r
}\r
\r
emit SaleStopped();\r
}\r
\r
function acceptBid(address buyer) external onlyOwner {\r
require(saleActive, "Sale is not active");\r
require(buyerIndex[buyer] > 0, "Buyer not found in the list");\r
\r
uint256 index = buyerIndex[buyer] - 1; // Convert to 0-based index\r
require(buyers[index].exists, "Buyer does not exist");\r
\r
uint256 bidAmount = buyers[index].bidAmount;\r
_transferOwnershipWithSale(buyer, bidAmount);\r
emit BidAccepted(buyer, bidAmount);\r
}\r
\r
function acceptHighestBid() external onlyOwner {\r
require(saleActive, "Sale is not active");\r
\r
address highestBidder;\r
uint256 highestBid = 0;\r
\r
for (uint256 i = 0; i < buyers.length; i++) {\r
if (buyers[i].exists && buyers[i].bidAmount > highestBid) {\r
highestBid = buyers[i].bidAmount;\r
highestBidder = buyers[i].buyerAddress;\r
}\r
}\r
\r
require(highestBidder != address(0), "No valid bids found");\r
require(highestBid > 0, "Highest bid must be greater than zero");\r
\r
_transferOwnershipWithSale(highestBidder, highestBid);\r
emit BidAccepted(highestBidder, highestBid);\r
}\r
\r
function placeBid() external payable {\r
require(saleActive, "Sale is not active");\r
require(msg.value > 0, "Bid amount must be greater than 0");\r
require(msg.value >= askingPrice / 2, "Must deposit at least half of asking price");\r
require(buyerIndex[msg.sender] == 0, "Already placed a bid");\r
\r
uint256 activeCount = _getActiveBuyersCount();\r
require(activeCount < MAX_BUYERS, "Maximum buyers reached");\r
\r
uint256 emptySlot = _findEmptySlot();\r
if (emptySlot != type(uint256).max) {\r
buyers[emptySlot] = Buyer({\r
buyerAddress: msg.sender,\r
bidAmount: msg.value,\r
exists: true\r
});\r
buyerIndex[msg.sender] = emptySlot + 1;\r
} else {\r
buyers.push(Buyer({\r
buyerAddress: msg.sender,\r
bidAmount: msg.value,\r
exists: true\r
}));\r
buyerIndex[msg.sender] = buyers.length;\r
}\r
\r
emit BidPlaced(msg.sender, msg.value);\r
\r
if (msg.value >= askingPrice) {\r
_transferOwnershipWithSale(msg.sender, msg.value);\r
}\r
}\r
function setBidWithdrawalAllowed(bool allowed) external onlyOwner {\r
require(saleActive, "Sale is not active");\r
allowBidWithdrawal = allowed;\r
emit WithdrawalPermissionChanged(allowed);\r
}\r
function getBuyersCount() external view returns (uint256) {\r
return _getActiveBuyersCount();\r
}\r
// ============= Refund Functions ============\r
function claimFailedRefund() external nonReentrant {\r
FailedRefund storage refund = failedRefunds[msg.sender];\r
require(refund.amount > 0, "No failed refund found");\r
require(!refund.claimed, "Refund already claimed");\r
\r
uint256 refundAmount = refund.amount;\r
refund.claimed = true;\r
refund.amount = 0;\r
\r
(bool success, ) = payable(msg.sender).call{value: refundAmount}("");\r
require(success, "Refund claim failed");\r
\r
emit RefundClaimed(msg.sender, refundAmount);\r
}\r
\r
function processManualRefund(address buyer) external onlyOwner nonReentrant {\r
FailedRefund storage refund = failedRefunds[buyer];\r
require(refund.amount > 0, "No failed refund found");\r
require(!refund.claimed, "Refund already claimed");\r
\r
uint256 refundAmount = refund.amount;\r
refund.claimed = true;\r
refund.amount = 0;\r
\r
(bool success, ) = payable(buyer).call{value: refundAmount}("");\r
require(success, "Manual refund failed");\r
\r
emit ManualRefundProcessed(buyer, refundAmount);\r
}\r
\r
function getPendingRefundsTotal() external view returns (uint256) {\r
uint256 total = 0;\r
for (uint256 i = 0; i < failedRefundAddresses.length; i++) {\r
FailedRefund storage refund = failedRefunds[failedRefundAddresses[i]];\r
if (!refund.claimed && refund.amount > 0) {\r
total += refund.amount;\r
}\r
}\r
return total;\r
}\r
\r
// ============= Subscription Functions ============\r
function createSubscriptionPlan(uint256 duration, uint256 price) external onlyOwner returns (uint256) {\r
uint256 planId = nextPlanId++;\r
subscriptionPlans[planId] = SubscriptionPlan({\r
duration: duration,\r
price: price,\r
active: true\r
});\r
\r
emit SubscriptionPlanCreated(planId, duration, price);\r
return planId;\r
}\r
\r
function updateSubscriptionPlan(uint256 planId, uint256 duration, uint256 price, bool active) external onlyOwner {\r
require(subscriptionPlans[planId].duration > 0, "Plan does not exist");\r
\r
subscriptionPlans[planId] = SubscriptionPlan({\r
duration: duration,\r
price: price,\r
active: active\r
});\r
\r
emit SubscriptionPlanUpdated(planId, duration, price, active);\r
}\r
\r
function subscribeToMedia(uint256 mediaId, uint256 planId) external payable nonReentrant {\r
require(media[mediaId].owner != address(0), "Media does not exist");\r
SubscriptionPlan storage plan = subscriptionPlans[planId];\r
require(plan.active, "Subscription plan is not active");\r
require(msg.value >= plan.price, "Insufficient payment for subscription");\r
\r
subscriptions[msg.sender][mediaId] = Subscription({\r
subscriber: msg.sender,\r
startTime: block.timestamp,\r
endTime: block.timestamp + plan.duration,\r
mediaId: mediaId,\r
active: true\r
});\r
\r
emit Subscribed(msg.sender, mediaId, planId, block.timestamp + plan.duration);\r
\r
// Refund excess payment\r
if (msg.value > plan.price) {\r
uint256 refundAmount = msg.value - plan.price;\r
(bool success, ) = payable(msg.sender).call{value: refundAmount}("");\r
require(success, "Refund failed");\r
}\r
}\r
\r
function setAuthorizedReader(address reader, bool authorized) external onlyOwner {\r
authorizedReaders[reader] = authorized;\r
emit AuthorizedReaderUpdated(reader, authorized);\r
}\r
\r
// ============= Emergency & Admin Functions ============\r
function initiateEmergencyWithdraw() external onlyOwner {\r
emergencyWithdrawTimestamp = block.timestamp;\r
emit EmergencyWithdrawInitiated(block.timestamp, block.timestamp + EMERGENCY_DELAY);\r
}\r
\r
function emergencyWithdraw() external onlyOwner nonReentrant {\r
require(emergencyWithdrawTimestamp > 0, "Emergency withdraw not initiated");\r
require(block.timestamp >= emergencyWithdrawTimestamp + EMERGENCY_DELAY, "Emergency delay not passed");\r
\r
uint256 balance = address(this).balance;\r
emergencyWithdrawTimestamp = 0;\r
\r
(bool success, ) = payable(owner()).call{value: balance}("");\r
require(success, "Emergency withdraw failed");\r
\r
emit EmergencyWithdrawExecuted(balance, block.timestamp);\r
}\r
\r
function withdrawTokens(address to, uint256 amount) external onlyOwner {\r
require(to != address(0), "Invalid address");\r
require(amount > 0, "Amount must be greater than 0");\r
require(address(this).balance >= withdrawalFee, "Insufficient ETH for withdrawal fee");\r
\r
// Pay withdrawal fee\r
(bool feeSuccess, ) = payable(feeReceiver).call{value: withdrawalFee}("");\r
require(feeSuccess, "Fee payment failed");\r
\r
// Transfer ChainGallery tokens\r
bool tokenSuccess = chainGalleryToken.transfer(to, amount);\r
require(tokenSuccess, "Token transfer failed");\r
\r
emit TokensWithdrawn(to, amount, withdrawalFee);\r
}\r
\r
// ============= Utility Functions ============\r
function getBuyers() external view returns (Buyer[] memory) {\r
return buyers;\r
}\r
\r
function getFailedRefundAddresses() external view returns (address[] memory) {\r
return failedRefundAddresses;\r
}\r
\r
function getFailedRefund(address buyer) external view returns (FailedRefund memory) {\r
return failedRefunds[buyer];\r
}\r
//======================\r
function getActiveBuyers() external view returns (Buyer[] memory) {\r
uint256 activeCount = _getActiveBuyersCount();\r
Buyer[] memory activeBuyers = new Buyer[](activeCount);\r
uint256 index = 0;\r
\r
for (uint256 i = 0; i < buyers.length; i++) {\r
if (buyers[i].exists) {\r
activeBuyers[index] = buyers[i];\r
index++;\r
}\r
}\r
\r
return activeBuyers;\r
}\r
function getMinimumDeposit() external view returns (uint256) {\r
return askingPrice / 2;\r
}\r
\r
function getBuyersStats() external view returns (uint256 active, uint256 totalSlots) {\r
uint256 activeCount = _getActiveBuyersCount();\r
return (activeCount, buyers.length);\r
}\r
//=========================================\r
function _hasValidSubscription(address subscriber, uint256 mediaId) internal view returns (bool) {\r
Subscription storage sub = subscriptions[subscriber][mediaId];\r
return sub.active && sub.endTime > block.timestamp;\r
}\r
\r
\r
function hasAccess(address subscriber, uint256 mediaId) external view returns (bool) {\r
// 1️⃣ Media must exist\r
if (media[mediaId].owner == address(0)) {\r
return false;\r
}\r
\r
// 2️⃣ Check all access methods\r
if (\r
subscriber == owner() ||\r
subscriber == media[mediaId].owner ||\r
_hasValidSubscription(subscriber, mediaId) || \r
authorizedReaders[subscriber] ||\r
hasGroupAccess(subscriber, mediaId)\r
) {\r
return true;\r
}\r
\r
return false;\r
}\r
// Add this function to your MediaOnChainStorage3 contract\r
function getSubscriptionInfo(address subscriber, uint256 mediaId) \r
public \r
view \r
returns (\r
address subscriber_,\r
uint256 startTime_,\r
uint256 endTime_,\r
uint256 mediaId_,\r
bool active_\r
) \r
{\r
Subscription memory sub = subscriptions[subscriber][mediaId];\r
return (\r
sub.subscriber,\r
sub.startTime,\r
sub.endTime,\r
sub.mediaId,\r
sub.active\r
);\r
}\r
\r
// Optional: Add a function to get all active subscriptions for a user\r
function getUserActiveSubscriptions(address subscriber) \r
public \r
view \r
returns (uint256[] memory activeMediaIds) \r
{\r
// This would require tracking user subscriptions, but for now we can return media IDs where user has active subscriptions\r
// You might need to implement proper tracking for this\r
uint256 mediaCount = getMediaCount();\r
uint256 activeCount = 0;\r
\r
// First pass: count active subscriptions\r
for (uint256 i = 0; i < mediaCount; i++) {\r
if (subscriptions[subscriber][i].active && subscriptions[subscriber][i].endTime > block.timestamp) {\r
activeCount++;\r
}\r
}\r
\r
// Second pass: populate array\r
uint256[] memory mediaIds = new uint256[](activeCount);\r
uint256 index = 0;\r
for (uint256 i = 0; i < mediaCount; i++) {\r
if (subscriptions[subscriber][i].active && subscriptions[subscriber][i].endTime > block.timestamp) {\r
mediaIds[index] = i;\r
index++;\r
}\r
}\r
\r
return mediaIds;\r
}\r
function grantFreeSubscription(address subscriber, uint256 mediaId, uint256 duration) external onlyOwner {\r
(address mediaOwner,,,,,,,) = getMedia(mediaId);\r
require(mediaOwner != address(0), "Media does not exist");\r
\r
Subscription storage subscription = subscriptions[subscriber][mediaId];\r
uint256 currentEndTime = subscription.endTime > block.timestamp ? subscription.endTime : block.timestamp;\r
\r
subscription.subscriber = subscriber;\r
subscription.mediaId = mediaId;\r
subscription.startTime = block.timestamp;\r
subscription.endTime = currentEndTime + duration;\r
subscription.active = true;\r
\r
emit Subscribed(subscriber, mediaId, 0, subscription.endTime);\r
}\r
//============================EXTRA============================\r
// Add these to your contract\r
struct GroupSubscription {\r
uint256 groupId;\r
string groupName;\r
uint256[] mediaIds;\r
uint256 price;\r
uint256 duration;\r
bool active;\r
address owner;\r
}\r
\r
mapping(uint256 => GroupSubscription) public groupSubscriptions;\r
mapping(address => mapping(uint256 => uint256)) public groupSubscriptionExpiry; // user => groupId => expiryTime\r
uint256 public nextGroupId;\r
\r
// Events\r
event GroupSubscriptionCreated(uint256 groupId, string groupName, uint256 price, uint256 duration, address owner);\r
event GroupSubscribed(address subscriber, uint256 groupId, uint256 expiryTime);\r
\r
// Create a new group subscription (owner only)\r
function createGroupSubscription(\r
string memory groupName,\r
uint256[] memory mediaIds,\r
uint256 price,\r
uint256 duration\r
) public onlyOwner returns (uint256) {\r
uint256 groupId = nextGroupId++;\r
\r
groupSubscriptions[groupId] = GroupSubscription({\r
groupId: groupId,\r
groupName: groupName,\r
mediaIds: mediaIds,\r
price: price,\r
duration: duration,\r
active: true,\r
owner: msg.sender\r
});\r
\r
emit GroupSubscriptionCreated(groupId, groupName, price, duration, msg.sender);\r
return groupId;\r
}\r
\r
// Subscribe to a group\r
function subscribeToGroup(uint256 groupId) public payable {\r
GroupSubscription storage group = groupSubscriptions[groupId];\r
require(group.active, "Group subscription not active");\r
require(msg.value >= group.price, "Insufficient payment");\r
\r
uint256 expiryTime = block.timestamp + group.duration;\r
groupSubscriptionExpiry[msg.sender][groupId] = expiryTime;\r
\r
emit GroupSubscribed(msg.sender, groupId, expiryTime);\r
\r
// Refund excess payment\r
if (msg.value > group.price) {\r
payable(msg.sender).transfer(msg.value - group.price);\r
}\r
}\r
\r
modifier canAccessMedia(uint256 mediaId) {\r
require(media[mediaId].owner != address(0), "Media does not exist");\r
\r
address mediaOwner = media[mediaId].owner;\r
bool hasAccess_ = (\r
msg.sender == owner() || \r
msg.sender == mediaOwner || \r
_hasActiveSubscription(msg.sender, mediaId) ||\r
authorizedReaders[msg.sender] ||\r
_hasGroupAccess(msg.sender, mediaId) // Use internal version for gas efficiency\r
);\r
\r
require(hasAccess_, "No access to this media");\r
_;\r
}\r
\r
// Internal version for modifier use (more gas efficient)\r
function _hasGroupAccess(address subscriber, uint256 mediaId) internal view returns (bool) {\r
for (uint256 i = 0; i < nextGroupId; i++) {\r
if (groupSubscriptions[i].active && groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
GroupSubscription storage group = groupSubscriptions[i];\r
for (uint256 j = 0; j < group.mediaIds.length; j++) {\r
if (group.mediaIds[j] == mediaId) {\r
return true;\r
}\r
}\r
}\r
}\r
return false;\r
}\r
\r
// Keep the public version for external calls\r
function hasGroupAccess(address subscriber, uint256 mediaId) public view returns (bool) {\r
return _hasGroupAccess(subscriber, mediaId);\r
}\r
\r
// Get user's active group subscriptions\r
function getUserGroupSubscriptions(address subscriber) public view returns (uint256[] memory activeGroups) {\r
uint256 count = 0;\r
\r
// Count active groups\r
for (uint256 i = 0; i < nextGroupId; i++) {\r
if (groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
count++;\r
}\r
}\r
\r
// Populate array\r
uint256[] memory groupIds = new uint256[](count);\r
uint256 index = 0;\r
for (uint256 i = 0; i < nextGroupId; i++) {\r
if (groupSubscriptionExpiry[subscriber][i] > block.timestamp) {\r
groupIds[index] = i;\r
index++;\r
}\r
}\r
\r
return groupIds;\r
}\r
\r
// Get group details\r
function getGroupSubscription(uint256 groupId) public view returns (\r
string memory groupName,\r
uint256[] memory mediaIds,\r
uint256 price,\r
uint256 duration,\r
bool active,\r
uint256 mediaCount\r
) {\r
GroupSubscription storage group = groupSubscriptions[groupId];\r
return (\r
group.groupName,\r
group.mediaIds,\r
group.price,\r
group.duration,\r
group.active,\r
group.mediaIds.length\r
);\r
}\r
\r
// Update group subscription (owner only)\r
function updateGroupSubscription(\r
uint256 groupId,\r
string memory groupName,\r
uint256[] memory mediaIds,\r
uint256 price,\r
uint256 duration,\r
bool active\r
) public onlyOwner {\r
GroupSubscription storage group = groupSubscriptions[groupId];\r
group.groupName = groupName;\r
group.mediaIds = mediaIds;\r
group.price = price;\r
group.duration = duration;\r
group.active = active;\r
}\r
//============================================================\r
\r
// Accept ETH transfers\r
receive() external payable {}\r
}"
},
"@openzeppelin/contracts/security/ReentrancyGuard.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
"
},
"@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)
pragma solidity >=0.4.16;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"@openzeppelin/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"remappings": []
}
}}
Submitted on: 2025-10-30 12:17:17
Comments
Log in to comment.
No comments yet.