1+ // SPDX-License-Identifier: MIT
2+ pragma solidity ^ 0.8.0 ;
3+
4+ import "../interfaces/IPackageRegistry.sol " ;
5+ import "../structs/PackageStructs.sol " ;
6+
7+ /// @title PackageRegistry
8+ /// @notice On-chain registry for EthPM v3 packages (EIP-2678)
9+ /// @dev Stores package references with content-addressable URIs (IPFS)
10+ contract PackageRegistry is IPackageRegistry {
11+
12+ // Package name => version => manifest URI (IPFS/Swarm)
13+ mapping (string => mapping (string => string )) private packages;
14+
15+ // Package name => owner address
16+ mapping (string => address ) private packageOwners;
17+
18+ // Package name => all versions
19+ mapping (string => string []) private packageVersions;
20+
21+ // Events
22+ event PackagePublished (string indexed name , string version , string manifestURI , address publisher );
23+ event PackageOwnershipTransferred (string indexed name , address previousOwner , address newOwner );
24+
25+ /// @notice Publish a package to the registry
26+ /// @param name Package name (must be lowercase, numbers, hyphens only)
27+ /// @param version Package version (semver format)
28+ /// @param manifestURI Content-addressable URI (ipfs:// or bzz://)
29+ function publish (string calldata name , string calldata version , string calldata manifestURI ) external override {
30+ require (bytes (name).length > 0 , "Package name cannot be empty " );
31+ require (bytes (version).length > 0 , "Version cannot be empty " );
32+ require (bytes (manifestURI).length > 0 , "Manifest URI cannot be empty " );
33+ require (_isValidPackageName (name), "Invalid package name format " );
34+
35+ // Check ownership
36+ if (packageOwners[name] == address (0 )) {
37+ // First time publishing this package
38+ packageOwners[name] = msg .sender ;
39+ } else {
40+ // Only owner can publish new versions
41+ require (packageOwners[name] == msg .sender , "Only package owner can publish " );
42+ }
43+
44+ // Check if version already exists
45+ require (bytes (packages[name][version]).length == 0 , "Version already exists " );
46+
47+ // Store package
48+ packages[name][version] = manifestURI;
49+ packageVersions[name].push (version);
50+
51+ emit PackagePublished (name, version, manifestURI, msg .sender );
52+ }
53+
54+ /// @notice Get package manifest URI
55+ /// @param name Package name
56+ /// @param version Package version
57+ /// @return manifestURI The content-addressable URI of the package manifest
58+ function getPackageURI (string calldata name , string calldata version ) external view override returns (string memory ) {
59+ string memory uri = packages[name][version];
60+ require (bytes (uri).length > 0 , "Package version does not exist " );
61+ return uri;
62+ }
63+
64+ /// @notice Check if package version exists
65+ /// @param name Package name
66+ /// @param version Package version
67+ /// @return exists True if package exists
68+ function packageExists (string calldata name , string calldata version ) external view override returns (bool ) {
69+ return bytes (packages[name][version]).length > 0 ;
70+ }
71+
72+ /// @notice Get all versions of a package
73+ /// @param name Package name
74+ /// @return versions Array of all versions
75+ function getVersions (string calldata name ) external view returns (string [] memory ) {
76+ return packageVersions[name];
77+ }
78+
79+ /// @notice Get package owner
80+ /// @param name Package name
81+ /// @return owner Address of package owner
82+ function getOwner (string calldata name ) external view returns (address ) {
83+ return packageOwners[name];
84+ }
85+
86+ /// @notice Transfer package ownership
87+ /// @param name Package name
88+ /// @param newOwner New owner address
89+ function transferOwnership (string calldata name , address newOwner ) external {
90+ require (packageOwners[name] == msg .sender , "Only owner can transfer ownership " );
91+ require (newOwner != address (0 ), "New owner cannot be zero address " );
92+
93+ address previousOwner = packageOwners[name];
94+ packageOwners[name] = newOwner;
95+
96+ emit PackageOwnershipTransferred (name, previousOwner, newOwner);
97+ }
98+
99+ /// @notice Validate package name format (lowercase, numbers, hyphens)
100+ /// @param name Package name to validate
101+ /// @return valid True if name is valid
102+ function _isValidPackageName (string calldata name ) private pure returns (bool ) {
103+ bytes memory nameBytes = bytes (name);
104+
105+ for (uint256 i = 0 ; i < nameBytes.length ; i++ ) {
106+ bytes1 char = nameBytes[i];
107+
108+ // Allow: a-z, 0-9, hyphen
109+ bool isLowercase = (char >= 0x61 && char <= 0x7A ); // a-z
110+ bool isNumber = (char >= 0x30 && char <= 0x39 ); // 0-9
111+ bool isHyphen = (char == 0x2D ); // -
112+
113+ if (! isLowercase && ! isNumber && ! isHyphen) {
114+ return false ;
115+ }
116+ }
117+
118+ return true ;
119+ }
120+ }
0 commit comments