feat: Goa GEL Blockchain e-Licensing Platform - Full Stack Implementation
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
This commit is contained in:
182
blockchain/contracts/ApprovalManager.sol
Normal file
182
blockchain/contracts/ApprovalManager.sol
Normal file
@@ -0,0 +1,182 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title ApprovalManager
|
||||
* @notice Manages and records approval actions on the blockchain
|
||||
* @dev Provides immutable audit trail for license approvals
|
||||
*/
|
||||
contract ApprovalManager is Ownable {
|
||||
enum ApprovalStatus {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
REJECTED,
|
||||
CHANGES_REQUESTED,
|
||||
INVALIDATED
|
||||
}
|
||||
|
||||
struct Approval {
|
||||
bytes32 id;
|
||||
string requestId;
|
||||
address departmentAddress;
|
||||
ApprovalStatus status;
|
||||
string remarksHash;
|
||||
string[] documentHashes;
|
||||
uint256 timestamp;
|
||||
bool isValid;
|
||||
}
|
||||
|
||||
// Mapping from approval ID to Approval struct
|
||||
mapping(bytes32 => Approval) private _approvals;
|
||||
|
||||
// Mapping from request ID to approval IDs
|
||||
mapping(string => bytes32[]) private _requestApprovals;
|
||||
|
||||
// Counter for generating unique approval IDs
|
||||
uint256 private _approvalCounter;
|
||||
|
||||
// Events
|
||||
event ApprovalRecorded(
|
||||
bytes32 indexed approvalId,
|
||||
string indexed requestId,
|
||||
address indexed departmentAddress,
|
||||
ApprovalStatus status,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
event ApprovalInvalidated(
|
||||
bytes32 indexed approvalId,
|
||||
string reason
|
||||
);
|
||||
|
||||
constructor() Ownable(msg.sender) {}
|
||||
|
||||
/**
|
||||
* @notice Record an approval action
|
||||
* @param requestId The license request ID
|
||||
* @param departmentAddress The address of the approving department
|
||||
* @param status The approval status
|
||||
* @param remarksHash Hash of the approval remarks
|
||||
* @param documentHashes Array of document hashes that were reviewed
|
||||
* @return approvalId The unique ID of the recorded approval
|
||||
*/
|
||||
function recordApproval(
|
||||
string calldata requestId,
|
||||
address departmentAddress,
|
||||
ApprovalStatus status,
|
||||
string calldata remarksHash,
|
||||
string[] calldata documentHashes
|
||||
) public onlyOwner returns (bytes32) {
|
||||
require(bytes(requestId).length > 0, "Request ID required");
|
||||
require(departmentAddress != address(0), "Invalid department address");
|
||||
|
||||
_approvalCounter++;
|
||||
bytes32 approvalId = keccak256(
|
||||
abi.encodePacked(requestId, departmentAddress, block.timestamp, _approvalCounter)
|
||||
);
|
||||
|
||||
Approval storage approval = _approvals[approvalId];
|
||||
approval.id = approvalId;
|
||||
approval.requestId = requestId;
|
||||
approval.departmentAddress = departmentAddress;
|
||||
approval.status = status;
|
||||
approval.remarksHash = remarksHash;
|
||||
approval.timestamp = block.timestamp;
|
||||
approval.isValid = true;
|
||||
|
||||
// Copy document hashes using loop (calldata to storage)
|
||||
for (uint256 i = 0; i < documentHashes.length; i++) {
|
||||
approval.documentHashes.push(documentHashes[i]);
|
||||
}
|
||||
|
||||
_requestApprovals[requestId].push(approvalId);
|
||||
|
||||
emit ApprovalRecorded(
|
||||
approvalId,
|
||||
requestId,
|
||||
departmentAddress,
|
||||
status,
|
||||
block.timestamp
|
||||
);
|
||||
|
||||
return approvalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get all approvals for a request
|
||||
* @param requestId The license request ID
|
||||
* @return Array of Approval structs
|
||||
*/
|
||||
function getRequestApprovals(string calldata requestId)
|
||||
public
|
||||
view
|
||||
returns (Approval[] memory)
|
||||
{
|
||||
bytes32[] memory approvalIds = _requestApprovals[requestId];
|
||||
Approval[] memory approvals = new Approval[](approvalIds.length);
|
||||
|
||||
for (uint256 i = 0; i < approvalIds.length; i++) {
|
||||
approvals[i] = _approvals[approvalIds[i]];
|
||||
}
|
||||
|
||||
return approvals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Invalidate an existing approval
|
||||
* @param approvalId The approval ID to invalidate
|
||||
* @param reason The reason for invalidation
|
||||
*/
|
||||
function invalidateApproval(bytes32 approvalId, string calldata reason)
|
||||
public
|
||||
onlyOwner
|
||||
{
|
||||
require(_approvals[approvalId].isValid, "Approval not found or already invalid");
|
||||
|
||||
_approvals[approvalId].isValid = false;
|
||||
_approvals[approvalId].status = ApprovalStatus.INVALIDATED;
|
||||
|
||||
emit ApprovalInvalidated(approvalId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify if approval remarks match the stored hash
|
||||
* @param approvalId The approval ID
|
||||
* @param remarksHash The hash to verify
|
||||
* @return True if the hashes match
|
||||
*/
|
||||
function verifyApproval(bytes32 approvalId, string calldata remarksHash)
|
||||
public
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
Approval memory approval = _approvals[approvalId];
|
||||
return approval.isValid &&
|
||||
keccak256(bytes(approval.remarksHash)) == keccak256(bytes(remarksHash));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get details of a specific approval
|
||||
* @param approvalId The approval ID
|
||||
* @return The Approval struct
|
||||
*/
|
||||
function getApprovalDetails(bytes32 approvalId)
|
||||
public
|
||||
view
|
||||
returns (Approval memory)
|
||||
{
|
||||
require(_approvals[approvalId].timestamp > 0, "Approval not found");
|
||||
return _approvals[approvalId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the count of approvals for a request
|
||||
* @param requestId The license request ID
|
||||
* @return The number of approvals
|
||||
*/
|
||||
function getApprovalCount(string calldata requestId) public view returns (uint256) {
|
||||
return _requestApprovals[requestId].length;
|
||||
}
|
||||
}
|
||||
3
blockchain/contracts/CLAUDE.md
Normal file
3
blockchain/contracts/CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
<claude-mem-context>
|
||||
|
||||
</claude-mem-context>
|
||||
193
blockchain/contracts/DocumentChain.sol
Normal file
193
blockchain/contracts/DocumentChain.sol
Normal file
@@ -0,0 +1,193 @@
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
172
blockchain/contracts/LicenseNFT.sol
Normal file
172
blockchain/contracts/LicenseNFT.sol
Normal file
@@ -0,0 +1,172 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
|
||||
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title LicenseNFT
|
||||
* @notice ERC721 token representing government-issued licenses
|
||||
* @dev Each license is minted as an NFT with associated metadata
|
||||
*/
|
||||
contract LicenseNFT is ERC721, ERC721URIStorage, Ownable {
|
||||
uint256 private _nextTokenId;
|
||||
|
||||
// Mapping from request ID to token ID
|
||||
mapping(string => uint256) private _requestToToken;
|
||||
|
||||
// Mapping from token ID to request ID
|
||||
mapping(uint256 => string) private _tokenToRequest;
|
||||
|
||||
// Mapping to track revoked licenses
|
||||
mapping(uint256 => bool) private _revokedTokens;
|
||||
|
||||
// Mapping to store revocation reasons
|
||||
mapping(uint256 => string) private _revocationReasons;
|
||||
|
||||
// Mapping to store license metadata URI
|
||||
mapping(uint256 => string) private _metadataUris;
|
||||
|
||||
// Events
|
||||
event LicenseMinted(
|
||||
uint256 indexed tokenId,
|
||||
address indexed to,
|
||||
string requestId,
|
||||
string metadataUri
|
||||
);
|
||||
|
||||
event LicenseRevoked(
|
||||
uint256 indexed tokenId,
|
||||
string reason
|
||||
);
|
||||
|
||||
constructor() ERC721("Goa Government License", "GOA-LIC") Ownable(msg.sender) {}
|
||||
|
||||
/**
|
||||
* @notice Mint a new license NFT
|
||||
* @param to The address to mint the token to
|
||||
* @param requestId The associated license request ID
|
||||
* @param metadataUri The URI containing license metadata
|
||||
* @return tokenId The ID of the newly minted token
|
||||
*/
|
||||
function mint(
|
||||
address to,
|
||||
string calldata requestId,
|
||||
string calldata metadataUri
|
||||
) public onlyOwner returns (uint256) {
|
||||
require(bytes(requestId).length > 0, "Request ID required");
|
||||
require(_requestToToken[requestId] == 0, "License already minted for this request");
|
||||
|
||||
uint256 tokenId = ++_nextTokenId;
|
||||
|
||||
_safeMint(to, tokenId);
|
||||
_setTokenURI(tokenId, metadataUri);
|
||||
|
||||
_requestToToken[requestId] = tokenId;
|
||||
_tokenToRequest[tokenId] = requestId;
|
||||
_metadataUris[tokenId] = metadataUri;
|
||||
|
||||
emit LicenseMinted(tokenId, to, requestId, metadataUri);
|
||||
|
||||
return tokenId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the token ID for a request
|
||||
* @param requestId The license request ID
|
||||
* @return The token ID (0 if not found)
|
||||
*/
|
||||
function tokenOfRequest(string calldata requestId) public view returns (uint256) {
|
||||
return _requestToToken[requestId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the request ID for a token
|
||||
* @param tokenId The token ID
|
||||
* @return The request ID
|
||||
*/
|
||||
function requestOfToken(uint256 tokenId) public view returns (string memory) {
|
||||
require(_ownerOf(tokenId) != address(0), "Token does not exist");
|
||||
return _tokenToRequest[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if a token exists
|
||||
* @param tokenId The token ID to check
|
||||
* @return True if the token exists
|
||||
*/
|
||||
function exists(uint256 tokenId) public view returns (bool) {
|
||||
return _ownerOf(tokenId) != address(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revoke a license
|
||||
* @param tokenId The token ID to revoke
|
||||
* @param reason The reason for revocation
|
||||
*/
|
||||
function revoke(uint256 tokenId, string calldata reason) public onlyOwner {
|
||||
require(_ownerOf(tokenId) != address(0), "Token does not exist");
|
||||
require(!_revokedTokens[tokenId], "License already revoked");
|
||||
|
||||
_revokedTokens[tokenId] = true;
|
||||
_revocationReasons[tokenId] = reason;
|
||||
|
||||
emit LicenseRevoked(tokenId, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if a license is revoked
|
||||
* @param tokenId The token ID to check
|
||||
* @return True if the license is revoked
|
||||
*/
|
||||
function isRevoked(uint256 tokenId) public view returns (bool) {
|
||||
return _revokedTokens[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the revocation reason for a license
|
||||
* @param tokenId The token ID
|
||||
* @return The revocation reason
|
||||
*/
|
||||
function getRevocationReason(uint256 tokenId) public view returns (string memory) {
|
||||
return _revocationReasons[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the metadata URI for a token
|
||||
* @param tokenId The token ID
|
||||
* @return The metadata URI
|
||||
*/
|
||||
function getMetadata(uint256 tokenId) public view returns (string memory) {
|
||||
require(_ownerOf(tokenId) != address(0), "Token does not exist");
|
||||
return _metadataUris[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the total number of minted licenses
|
||||
* @return The total count
|
||||
*/
|
||||
function totalSupply() public view returns (uint256) {
|
||||
return _nextTokenId;
|
||||
}
|
||||
|
||||
// Override required functions
|
||||
function tokenURI(uint256 tokenId)
|
||||
public
|
||||
view
|
||||
override(ERC721, ERC721URIStorage)
|
||||
returns (string memory)
|
||||
{
|
||||
return super.tokenURI(tokenId);
|
||||
}
|
||||
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
override(ERC721, ERC721URIStorage)
|
||||
returns (bool)
|
||||
{
|
||||
return super.supportsInterface(interfaceId);
|
||||
}
|
||||
}
|
||||
188
blockchain/contracts/WorkflowRegistry.sol
Normal file
188
blockchain/contracts/WorkflowRegistry.sol
Normal file
@@ -0,0 +1,188 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.20;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title WorkflowRegistry
|
||||
* @notice Registers and tracks workflow definitions on-chain
|
||||
* @dev Placeholder for future workflow verification capabilities
|
||||
*/
|
||||
contract WorkflowRegistry is Ownable {
|
||||
struct WorkflowDefinition {
|
||||
bytes32 id;
|
||||
string workflowType;
|
||||
string name;
|
||||
bytes32 definitionHash;
|
||||
uint256 version;
|
||||
uint256 timestamp;
|
||||
bool isActive;
|
||||
}
|
||||
|
||||
// Mapping from workflow ID to definition
|
||||
mapping(bytes32 => WorkflowDefinition) private _workflows;
|
||||
|
||||
// Mapping from workflow type to latest workflow ID
|
||||
mapping(string => bytes32) private _latestWorkflows;
|
||||
|
||||
// Array of all workflow IDs
|
||||
bytes32[] private _workflowIds;
|
||||
|
||||
// Counter for unique IDs
|
||||
uint256 private _workflowCounter;
|
||||
|
||||
// Events
|
||||
event WorkflowRegistered(
|
||||
bytes32 indexed workflowId,
|
||||
string indexed workflowType,
|
||||
string name,
|
||||
bytes32 definitionHash,
|
||||
uint256 version
|
||||
);
|
||||
|
||||
event WorkflowDeactivated(bytes32 indexed workflowId);
|
||||
|
||||
event WorkflowActivated(bytes32 indexed workflowId);
|
||||
|
||||
constructor() Ownable(msg.sender) {}
|
||||
|
||||
/**
|
||||
* @notice Register a new workflow definition
|
||||
* @param workflowType The type of workflow (e.g., "RESORT_LICENSE")
|
||||
* @param name Human-readable name
|
||||
* @param definitionHash Hash of the workflow definition JSON
|
||||
* @return workflowId The unique workflow ID
|
||||
*/
|
||||
function registerWorkflow(
|
||||
string calldata workflowType,
|
||||
string calldata name,
|
||||
bytes32 definitionHash
|
||||
) public onlyOwner returns (bytes32) {
|
||||
require(bytes(workflowType).length > 0, "Workflow type required");
|
||||
require(bytes(name).length > 0, "Name required");
|
||||
require(definitionHash != bytes32(0), "Definition hash required");
|
||||
|
||||
_workflowCounter++;
|
||||
bytes32 workflowId = keccak256(
|
||||
abi.encodePacked(workflowType, _workflowCounter, block.timestamp)
|
||||
);
|
||||
|
||||
// Determine version
|
||||
uint256 version = 1;
|
||||
bytes32 latestId = _latestWorkflows[workflowType];
|
||||
if (latestId != bytes32(0)) {
|
||||
version = _workflows[latestId].version + 1;
|
||||
// Deactivate previous version
|
||||
_workflows[latestId].isActive = false;
|
||||
}
|
||||
|
||||
WorkflowDefinition storage workflow = _workflows[workflowId];
|
||||
workflow.id = workflowId;
|
||||
workflow.workflowType = workflowType;
|
||||
workflow.name = name;
|
||||
workflow.definitionHash = definitionHash;
|
||||
workflow.version = version;
|
||||
workflow.timestamp = block.timestamp;
|
||||
workflow.isActive = true;
|
||||
|
||||
_latestWorkflows[workflowType] = workflowId;
|
||||
_workflowIds.push(workflowId);
|
||||
|
||||
emit WorkflowRegistered(
|
||||
workflowId,
|
||||
workflowType,
|
||||
name,
|
||||
definitionHash,
|
||||
version
|
||||
);
|
||||
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get workflow definition by ID
|
||||
* @param workflowId The workflow ID
|
||||
* @return The WorkflowDefinition struct
|
||||
*/
|
||||
function getWorkflow(bytes32 workflowId)
|
||||
public
|
||||
view
|
||||
returns (WorkflowDefinition memory)
|
||||
{
|
||||
require(_workflows[workflowId].timestamp > 0, "Workflow not found");
|
||||
return _workflows[workflowId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get the latest active workflow for a type
|
||||
* @param workflowType The workflow type
|
||||
* @return The WorkflowDefinition struct
|
||||
*/
|
||||
function getLatestWorkflow(string calldata workflowType)
|
||||
public
|
||||
view
|
||||
returns (WorkflowDefinition memory)
|
||||
{
|
||||
bytes32 workflowId = _latestWorkflows[workflowType];
|
||||
require(workflowId != bytes32(0), "No workflow found for type");
|
||||
return _workflows[workflowId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify a workflow definition hash
|
||||
* @param workflowId The workflow ID
|
||||
* @param definitionHash The hash to verify
|
||||
* @return True if the hash matches
|
||||
*/
|
||||
function verifyWorkflow(bytes32 workflowId, bytes32 definitionHash)
|
||||
public
|
||||
view
|
||||
returns (bool)
|
||||
{
|
||||
WorkflowDefinition memory workflow = _workflows[workflowId];
|
||||
return workflow.isActive && workflow.definitionHash == definitionHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Deactivate a workflow
|
||||
* @param workflowId The workflow ID
|
||||
*/
|
||||
function deactivateWorkflow(bytes32 workflowId) public onlyOwner {
|
||||
require(_workflows[workflowId].timestamp > 0, "Workflow not found");
|
||||
require(_workflows[workflowId].isActive, "Workflow already inactive");
|
||||
|
||||
_workflows[workflowId].isActive = false;
|
||||
|
||||
emit WorkflowDeactivated(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Activate a workflow
|
||||
* @param workflowId The workflow ID
|
||||
*/
|
||||
function activateWorkflow(bytes32 workflowId) public onlyOwner {
|
||||
require(_workflows[workflowId].timestamp > 0, "Workflow not found");
|
||||
require(!_workflows[workflowId].isActive, "Workflow already active");
|
||||
|
||||
_workflows[workflowId].isActive = true;
|
||||
|
||||
emit WorkflowActivated(workflowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get total workflow count
|
||||
* @return The number of registered workflows
|
||||
*/
|
||||
function getWorkflowCount() public view returns (uint256) {
|
||||
return _workflowIds.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if a workflow is active
|
||||
* @param workflowId The workflow ID
|
||||
* @return True if active
|
||||
*/
|
||||
function isWorkflowActive(bytes32 workflowId) public view returns (bool) {
|
||||
return _workflows[workflowId].isActive;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user