Complete implementation of the Goa Government e-Licensing platform with: Backend: - NestJS API with JWT authentication - PostgreSQL database with Knex ORM - Redis caching and session management - MinIO document storage - Hyperledger Besu blockchain integration - Multi-department workflow system - Comprehensive API tests (266/282 passing) Frontend: - Angular 21 with standalone components - Angular Material + TailwindCSS UI - Visual workflow builder - Document upload with progress tracking - Blockchain explorer integration - Role-based dashboards (Admin, Department, Citizen) - E2E tests with Playwright (37 tests) Infrastructure: - Docker Compose orchestration - Blockscout blockchain explorer - Development and production configurations
194 lines
5.4 KiB
Solidity
194 lines
5.4 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
|
|
|
/**
|
|
* @title DocumentChain
|
|
* @notice Records and verifies document hashes on the blockchain
|
|
* @dev Provides tamper-proof document verification
|
|
*/
|
|
contract DocumentChain is Ownable {
|
|
struct DocumentRecord {
|
|
bytes32 id;
|
|
string requestId;
|
|
string documentId;
|
|
string hash;
|
|
uint256 version;
|
|
uint256 timestamp;
|
|
address uploadedBy;
|
|
}
|
|
|
|
// Mapping from document ID to its records
|
|
mapping(string => DocumentRecord[]) private _documentHistory;
|
|
|
|
// Mapping from document ID to latest hash
|
|
mapping(string => string) private _latestHashes;
|
|
|
|
// Mapping from hash to document ID (for reverse lookup)
|
|
mapping(string => string) private _hashToDocument;
|
|
|
|
// Counter for unique record IDs
|
|
uint256 private _recordCounter;
|
|
|
|
// Events
|
|
event DocumentRecorded(
|
|
bytes32 indexed recordId,
|
|
string indexed requestId,
|
|
string documentId,
|
|
string hash,
|
|
uint256 version,
|
|
uint256 timestamp
|
|
);
|
|
|
|
constructor() Ownable(msg.sender) {}
|
|
|
|
/**
|
|
* @notice Record a document hash
|
|
* @param requestId The license request ID
|
|
* @param documentId The document ID
|
|
* @param hash The document hash (e.g., SHA-256)
|
|
* @param version The document version
|
|
* @return recordId The unique record ID
|
|
*/
|
|
function recordDocumentHash(
|
|
string calldata requestId,
|
|
string calldata documentId,
|
|
string calldata hash,
|
|
uint256 version
|
|
) public onlyOwner returns (bytes32) {
|
|
require(bytes(requestId).length > 0, "Request ID required");
|
|
require(bytes(documentId).length > 0, "Document ID required");
|
|
require(bytes(hash).length > 0, "Hash required");
|
|
|
|
_recordCounter++;
|
|
bytes32 recordId = keccak256(
|
|
abi.encodePacked(documentId, version, block.timestamp, _recordCounter)
|
|
);
|
|
|
|
DocumentRecord memory record = DocumentRecord({
|
|
id: recordId,
|
|
requestId: requestId,
|
|
documentId: documentId,
|
|
hash: hash,
|
|
version: version,
|
|
timestamp: block.timestamp,
|
|
uploadedBy: msg.sender
|
|
});
|
|
|
|
_documentHistory[documentId].push(record);
|
|
_latestHashes[documentId] = hash;
|
|
_hashToDocument[hash] = documentId;
|
|
|
|
emit DocumentRecorded(
|
|
recordId,
|
|
requestId,
|
|
documentId,
|
|
hash,
|
|
version,
|
|
block.timestamp
|
|
);
|
|
|
|
return recordId;
|
|
}
|
|
|
|
/**
|
|
* @notice Verify if a document hash exists
|
|
* @param documentId The document ID
|
|
* @param hash The hash to verify
|
|
* @return True if the hash matches any recorded version
|
|
*/
|
|
function verifyDocumentHash(string calldata documentId, string calldata hash)
|
|
public
|
|
view
|
|
returns (bool)
|
|
{
|
|
DocumentRecord[] memory history = _documentHistory[documentId];
|
|
|
|
for (uint256 i = 0; i < history.length; i++) {
|
|
if (keccak256(bytes(history[i].hash)) == keccak256(bytes(hash))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @notice Verify if a hash is the latest version
|
|
* @param documentId The document ID
|
|
* @param hash The hash to verify
|
|
* @return True if the hash is the latest version
|
|
*/
|
|
function verifyLatestHash(string calldata documentId, string calldata hash)
|
|
public
|
|
view
|
|
returns (bool)
|
|
{
|
|
return keccak256(bytes(_latestHashes[documentId])) == keccak256(bytes(hash));
|
|
}
|
|
|
|
/**
|
|
* @notice Get the complete history of a document
|
|
* @param documentId The document ID
|
|
* @return Array of DocumentRecord structs
|
|
*/
|
|
function getDocumentHistory(string calldata documentId)
|
|
public
|
|
view
|
|
returns (DocumentRecord[] memory)
|
|
{
|
|
return _documentHistory[documentId];
|
|
}
|
|
|
|
/**
|
|
* @notice Get the latest hash for a document
|
|
* @param documentId The document ID
|
|
* @return The latest document hash
|
|
*/
|
|
function getLatestDocumentHash(string calldata documentId)
|
|
public
|
|
view
|
|
returns (string memory)
|
|
{
|
|
return _latestHashes[documentId];
|
|
}
|
|
|
|
/**
|
|
* @notice Get document ID by hash
|
|
* @param hash The document hash
|
|
* @return The document ID
|
|
*/
|
|
function getDocumentByHash(string calldata hash)
|
|
public
|
|
view
|
|
returns (string memory)
|
|
{
|
|
return _hashToDocument[hash];
|
|
}
|
|
|
|
/**
|
|
* @notice Get the version count for a document
|
|
* @param documentId The document ID
|
|
* @return The number of versions
|
|
*/
|
|
function getVersionCount(string calldata documentId) public view returns (uint256) {
|
|
return _documentHistory[documentId].length;
|
|
}
|
|
|
|
/**
|
|
* @notice Get a specific version of a document
|
|
* @param documentId The document ID
|
|
* @param version The version number (1-indexed)
|
|
* @return The DocumentRecord for that version
|
|
*/
|
|
function getDocumentVersion(string calldata documentId, uint256 version)
|
|
public
|
|
view
|
|
returns (DocumentRecord memory)
|
|
{
|
|
require(version > 0 && version <= _documentHistory[documentId].length, "Invalid version");
|
|
return _documentHistory[documentId][version - 1];
|
|
}
|
|
}
|