Spaces:
Running
Running
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.0; | |
| contract PureBi { | |
| // --- 0. BRANDING --- | |
| string public constant name = "PureBilingualVersation Research Registry"; | |
| string public constant symbol = "PURE BI-VERSATION"; | |
| // --- 1. IDENTITY & REPUTATION (PER DIALECT) --- | |
| mapping(address => bool) public isRegistered; | |
| // user -> dialect -> xp | |
| mapping(address => mapping(string => uint256)) public userXP; | |
| // user -> dialect -> role | |
| mapping(address => mapping(string => string)) public userRole; | |
| // Global admin checking | |
| mapping(address => bool) public isAdmin; | |
| address public labAdmin; // The Gasless Relayer (Python Backend) | |
| // --- 2. REGISTRY & LICENSING --- | |
| struct DialectEntry { | |
| uint256 id; | |
| string phrase; | |
| string dialect; | |
| string ipfsCid; | |
| address contributor; | |
| string licenseType; | |
| uint256 verificationCount; | |
| uint256 rejectionCount; | |
| bool isVerified; | |
| bool isBurned; | |
| } | |
| uint256 public entryCounter; | |
| mapping(uint256 => DialectEntry) public entries; | |
| // Consensus Tracking (entry_id -> user -> bool) | |
| mapping(uint256 => mapping(address => bool)) public hasVoted; | |
| // The Burn Ward Tracking | |
| mapping(uint256 => string) public burnReasons; | |
| mapping(uint256 => address[]) public rejectorsList; | |
| mapping(uint256 => uint256) public appealCount; | |
| mapping(uint256 => mapping(address => bool)) public hasAppealed; | |
| // --- 3. BACKUP SYSTEM --- | |
| struct DataSnapshot { | |
| uint256 timestamp; | |
| string dataType; | |
| string ipfsCid; | |
| string description; | |
| } | |
| mapping(string => DataSnapshot[]) public backups; | |
| // --- 4. EVENTS --- | |
| event EntryProposed(uint256 indexed id, string phrase, address indexed proposer); | |
| event EntryVerified(uint256 indexed id, address indexed verifier); | |
| event EntryRejected(uint256 indexed id, address indexed rejector, string reason); | |
| event EntryBurned(uint256 indexed id, address indexed contributor, string reason); | |
| event EntryAppealed(uint256 indexed id, address indexed appealer, bool success); | |
| event ReputationMinted(address indexed researcher, string dialect, uint256 amount); | |
| event RankUpgraded(address indexed researcher, string dialect, string newRole); | |
| event BackupCreated(string indexed dataKey, string ipfsCid, uint256 timestamp); | |
| constructor() { | |
| labAdmin = msg.sender; // The Python Backend Wallet | |
| isAdmin[msg.sender] = true; | |
| } | |
| // --- MODIFIERS --- | |
| modifier onlyRelayer() { | |
| require(msg.sender == labAdmin, "Only the Python Relayer can execute transactions"); | |
| _; | |
| } | |
| // --- CORE FUNCTIONS --- | |
| function proposeEntry(string memory _phrase, string memory _dialect, string memory _ipfsCid, string memory _license, address _contributor) public onlyRelayer returns (uint256) { | |
| entryCounter++; | |
| entries[entryCounter] = DialectEntry(entryCounter, _phrase, _dialect, _ipfsCid, _contributor, _license, 0, 0, false, false); | |
| emit EntryProposed(entryCounter, _phrase, _contributor); | |
| if (!isRegistered[_contributor]) { | |
| isRegistered[_contributor] = true; | |
| } | |
| // Auto-register brand new React users as Initiates in this dialect if they have no role | |
| if (bytes(userRole[_contributor][_dialect]).length == 0 && !isAdmin[_contributor]) { | |
| userRole[_contributor][_dialect] = "Initiate"; | |
| } | |
| return entryCounter; | |
| } | |
| function verifyEntry(uint256 _id, address _verifier) public onlyRelayer { | |
| DialectEntry storage entry = entries[_id]; | |
| require(!entry.isVerified && !entry.isBurned, "Entry already resolved"); | |
| require(entry.contributor != _verifier, "Cannot verify own entry"); | |
| require(!hasVoted[_id][_verifier], "User already voted"); | |
| string memory role = userRole[_verifier][entry.dialect]; | |
| bool isGod = isAdmin[_verifier]; | |
| bool isOracle = keccak256(bytes(role)) == keccak256(bytes("Oracle")); | |
| require(isGod || isOracle, "Only Admins or Oracles can verify"); | |
| hasVoted[_id][_verifier] = true; | |
| _mintReputation(_verifier, entry.dialect, 10); // +10 XP for doing the audit | |
| if (isGod) { | |
| _applyVerification(_id); | |
| } else { | |
| entry.verificationCount++; | |
| if (entry.verificationCount >= 2) { | |
| _applyVerification(_id); | |
| } | |
| } | |
| } | |
| function _applyVerification(uint256 _id) internal { | |
| entries[_id].isVerified = true; | |
| _mintReputation(entries[_id].contributor, entries[_id].dialect, 50); // +50 XP to the original submitter | |
| emit EntryVerified(_id, msg.sender); | |
| } | |
| function rejectEntry(uint256 _id, address _rejector, string memory _reason) public onlyRelayer { | |
| DialectEntry storage entry = entries[_id]; | |
| require(!entry.isVerified && !entry.isBurned, "Entry already resolved"); | |
| require(entry.contributor != _rejector, "Cannot reject own entry"); | |
| require(!hasVoted[_id][_rejector], "User already voted"); | |
| string memory role = userRole[_rejector][entry.dialect]; | |
| bool isGod = isAdmin[_rejector]; | |
| bool isOracle = keccak256(bytes(role)) == keccak256(bytes("Oracle")); | |
| require(isGod || isOracle, "Only Admins or Oracles can reject"); | |
| hasVoted[_id][_rejector] = true; | |
| rejectorsList[_id].push(_rejector); // Track for potential penalty | |
| _mintReputation(_rejector, entry.dialect, 10); // +10 XP for doing the audit | |
| emit EntryRejected(_id, _rejector, _reason); | |
| if (isGod) { | |
| _applyBurn(_id, _reason); | |
| } else { | |
| entry.rejectionCount++; | |
| if (entry.rejectionCount >= 3) { | |
| _applyBurn(_id, _reason); | |
| } | |
| } | |
| } | |
| function _applyBurn(uint256 _id, string memory _reason) internal { | |
| entries[_id].isBurned = true; | |
| burnReasons[_id] = _reason; | |
| address badActor = entries[_id].contributor; | |
| string memory dialect = entries[_id].dialect; | |
| // Burn -10 XP from the contributor | |
| if (userXP[badActor][dialect] >= 10) { | |
| userXP[badActor][dialect] -= 10; | |
| } else { | |
| userXP[badActor][dialect] = 0; | |
| } | |
| _updateRank(badActor, dialect); | |
| emit EntryBurned(_id, badActor, _reason); | |
| } | |
| function appealEntry(uint256 _id, address _appealer) public onlyRelayer { | |
| DialectEntry storage entry = entries[_id]; | |
| require(entry.isBurned, "Entry is not burned"); | |
| require(!hasAppealed[_id][_appealer], "User already appealed"); | |
| string memory role = userRole[_appealer][entry.dialect]; | |
| bool isGod = isAdmin[_appealer]; | |
| bool isOracle = keccak256(bytes(role)) == keccak256(bytes("Oracle")); | |
| require(isGod || isOracle, "Only Admins or Oracles can appeal"); | |
| hasAppealed[_id][_appealer] = true; | |
| appealCount[_id]++; | |
| // +10 XP for auditing the graveyard | |
| _mintReputation(_appealer, entry.dialect, 10); | |
| if (isGod || appealCount[_id] >= 2) { | |
| // Overturn the rejection! | |
| entry.isBurned = false; | |
| entry.isVerified = true; | |
| // Restore submitter's 50 XP + 10 XP penalty refund | |
| _mintReputation(entry.contributor, entry.dialect, 60); | |
| // Penalize the original rejectors -15 XP each | |
| address[] memory rejectors = rejectorsList[_id]; | |
| for(uint i=0; i<rejectors.length; i++) { | |
| address r = rejectors[i]; | |
| if (userXP[r][entry.dialect] >= 15) { | |
| userXP[r][entry.dialect] -= 15; | |
| } else { | |
| userXP[r][entry.dialect] = 0; | |
| } | |
| _updateRank(r, entry.dialect); | |
| } | |
| emit EntryAppealed(_id, _appealer, true); | |
| } else { | |
| emit EntryAppealed(_id, _appealer, false); | |
| } | |
| } | |
| function createBackup(string memory _dataKey, string memory _dataType, string memory _ipfsCid, string memory _description) public onlyRelayer { | |
| DataSnapshot memory newSnap = DataSnapshot(block.timestamp, _dataType, _ipfsCid, _description); | |
| backups[_dataKey].push(newSnap); | |
| emit BackupCreated(_dataKey, _ipfsCid, block.timestamp); | |
| } | |
| // --- INTERNAL REPUTATION & RANKING LOGIC --- | |
| function _mintReputation(address _user, string memory _dialect, uint256 _amount) internal { | |
| userXP[_user][_dialect] += _amount; | |
| emit ReputationMinted(_user, _dialect, _amount); | |
| _updateRank(_user, _dialect); | |
| } | |
| function _updateRank(address _user, string memory _dialect) internal { | |
| if (isAdmin[_user]) return; | |
| uint256 score = userXP[_user][_dialect]; | |
| string memory newRole; | |
| if (score >= 1500) { newRole = "Oracle"; } | |
| else if (score >= 800) { newRole = "Archivist"; } | |
| else if (score >= 300) { newRole = "Linguist"; } | |
| else if (score >= 100) { newRole = "Scout"; } | |
| else { newRole = "Initiate"; } | |
| if (keccak256(abi.encodePacked(userRole[_user][_dialect])) != keccak256(abi.encodePacked(newRole))) { | |
| userRole[_user][_dialect] = newRole; | |
| emit RankUpgraded(_user, _dialect, newRole); | |
| } | |
| } | |
| // --- MIGRATION QUERIES --- | |
| function batchMigrateXP(address[] memory _users, string[] memory _dialects, uint256[] memory _xps) public onlyRelayer { | |
| require(_users.length == _dialects.length && _users.length == _xps.length, "Array lengths mismatch"); | |
| for(uint i=0; i<_users.length; i++){ | |
| userXP[_users[i]][_dialects[i]] += _xps[i]; | |
| _updateRank(_users[i], _dialects[i]); | |
| emit ReputationMinted(_users[i], _dialects[i], _xps[i]); | |
| } | |
| } | |
| // --- HELPER QUERIES --- | |
| function getRole(address _user, string memory _dialect) public view returns (string memory) { | |
| if (isAdmin[_user]) return "Admin"; | |
| if (bytes(userRole[_user][_dialect]).length == 0) return "Initiate"; | |
| return userRole[_user][_dialect]; | |
| } | |
| function getXP(address _user, string memory _dialect) public view returns (uint256) { | |
| return userXP[_user][_dialect]; | |
| } | |
| } | |