Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions contracts/Registry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,15 @@ contract Registry is Initializable, AccessControlEnumerableUpgradeable {
address _dev,
uint64 flags,
string memory _version,
string[] memory _contentURIs
string[] memory _contentURIs,
string[] memory _tags
) external onlyAddPackageRole returns (Repo) {
Repo repo = Repo(ClonesUpgradeable.clone(repoImplementation));

// Registry must have permissions to create the first version
repo.initialize(address(this));

repo.newVersion(_version, _contentURIs);
repo.newVersion(_version, _contentURIs, _tags);

// Revoke permissions and grant to dev
repo.grantRole(repo.DEFAULT_ADMIN_ROLE(), _dev);
Expand Down
43 changes: 30 additions & 13 deletions contracts/Repo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ contract Repo is Initializable, AccessControlEnumerableUpgradeable {
mapping(bytes32 => uint256) public versionIdForSemantic;

/**
* @notice Algorithm to sort versions and derive "latest"
* - 0: Semver
* - 1: Alphabetical
* TBD
* @notice Map of tag hashes to versionId. Allows to map "latest" -> "v1.0.0".
* use getTag() for querying and setTag() for setting.
*/
uint256 public versionSorting;
mapping(bytes32 => uint256) internal versionIdByTag;

event NewVersion(uint256 versionId, string version, string[] contentURIs);

Expand All @@ -62,34 +60,48 @@ contract Repo is Initializable, AccessControlEnumerableUpgradeable {
* @param _version Refer to Version.version for details
* @param _contentURIs Refer to Version.contentURIs for details
*/
function newVersion(string memory _version, string[] memory _contentURIs) external onlyRole(CREATE_VERSION_ROLE) {
function newVersion(
string memory _version,
string[] memory _contentURIs,
string[] memory _tags
) external onlyRole(CREATE_VERSION_ROLE) {
require(_contentURIs.length > 0, "EMPTY_CONTENTURIS");

// Can only publish each version string once
bytes32 versionHash = semanticVersionHash(_version);
bytes32 versionHash = stringHash(_version);
require(versionIdForSemantic[versionHash] == 0, "REPO_EXISTENT_VERSION");

uint256 versionId = nextIdx++;
versions[versionId] = Version(_version, _contentURIs);
versionIdForSemantic[versionHash] = versionId;

for (uint256 i = 0; i < _tags.length; i++) {
_setTag(_tags[i], versionId);
}

emit NewVersion(versionId, _version, _contentURIs);
}

/**
* @notice Set the algorithm to sort versions and derive "latest"
* @param _versionSorting New version sorting algorithm
* @notice Set a tag to an existing version.
* @param _tag tag to set.
* @param _versionId version to point _tag to.
*/
function setVersionSorting(uint256 _versionSorting) external onlyRole(CREATE_VERSION_ROLE) {
versionSorting = _versionSorting;
function setTag(string memory _tag, uint256 _versionId) external onlyRole(CREATE_VERSION_ROLE) {
require(_versionId < nextIdx, "REPO_INEXISTENT_VERSION");
_setTag(_tag, _versionId);
}

function getTag(string memory _tag) public view returns (Version memory) {
return getByVersionId(versionIdByTag[stringHash(_tag)]);
}

function getLastPublished() public view returns (Version memory) {
return getByVersionId(nextIdx - 1);
}

function getBySemanticVersion(string memory _version) public view returns (Version memory) {
return getByVersionId(versionIdForSemantic[semanticVersionHash(_version)]);
return getByVersionId(versionIdForSemantic[stringHash(_version)]);
}

function getByVersionId(uint256 _versionId) public view returns (Version memory) {
Expand All @@ -101,7 +113,12 @@ contract Repo is Initializable, AccessControlEnumerableUpgradeable {
return nextIdx - 1;
}

function semanticVersionHash(string memory version) internal pure returns (bytes32) {
function _setTag(string memory _tag, uint256 _versionId) internal {
bytes32 tagHash = stringHash(_tag);
versionIdByTag[tagHash] = _versionId;
}

function stringHash(string memory version) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(version));
}
}
32 changes: 21 additions & 11 deletions test/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {expect} from "chai";
import {ethers, upgrades} from "hardhat";
import {BigNumber, Event} from "ethers";
import {Registry, PackageStruct} from "../typechain-types/Registry";
import {Repo, VersionStruct} from "../typechain-types/Repo";
import {Repo, VersionStruct, VersionStructOutput} from "../typechain-types/Repo";
import {RegistryV2Mock} from "../typechain-types/RegistryV2Mock";

interface RepoPackage {
Expand Down Expand Up @@ -59,27 +59,32 @@ describe("Registry", function () {
);

// Attempt to publish version with non-auth account
await expect(repoWithAdmin.newVersion(newVersion1.version, newVersion1.contentURIs)).to.be.revertedWith(
await expect(repoWithAdmin.newVersion(newVersion1.version, newVersion1.contentURIs, ["latest"])).to.be.revertedWith(
"AccessControl"
);

// Attempt to publish a version on an existing version str
await expect(repoWithDev.newVersion(newVersion1.version, newVersion1.contentURIs)).to.be.revertedWith(
await expect(repoWithDev.newVersion(newVersion1.version, newVersion1.contentURIs, ["latest"])).to.be.revertedWith(
"REPO_EXISTENT_VERSION"
);

// Publish a version on a different version str
const newVersionTx = await repoWithDev.newVersion(newVersion2.version, newVersion2.contentURIs, {
const newVersionTx = await repoWithDev.newVersion(newVersion2.version, newVersion2.contentURIs, ["latest"], {
from: addr1.address,
});
const newVersionReceipt = await newVersionTx.wait();

const newVersionEvent = getEvent(newVersionReceipt.events, "NewVersion");
expect(newVersionEvent.args!.version).to.equal(newVersion2.version, "Wrong event NewVersion.version");
expect(newVersionEvent.args!.contentURIs).to.deep.equal(newVersion2.contentURIs, "Wrong event NewVersion.contentURIs");
expect(newVersionEvent.args!.contentURIs).to.deep.equal(
newVersion2.contentURIs,
"Wrong event NewVersion.contentURIs"
);

// Assert that there are two version in the Repo contract
await assertRepoVersions(repoWithDev, [newVersion1, newVersion2]);

expect(toVersion(await repoWithDev.getTag("latest"))).to.deep.equal(newVersion2, "Wrong 'latest' tag");
});

it("public.dappnode registry publish one package and validate", async function () {
Expand Down Expand Up @@ -174,7 +179,7 @@ describe("Registry", function () {
const repoWithDev = (await ethers.getContractAt("Repo", newRepoAddress.address, dev)) as Repo;

// Publish a version on the new repo
const newVersionTx = await repoWithDev.newVersion(correctVersion.version, correctVersion.contentURIs);
const newVersionTx = await repoWithDev.newVersion(correctVersion.version, correctVersion.contentURIs, ["latest"]);
const newVersionReceipt = await newVersionTx.wait();

const newVersionEvent = getEvent(newVersionReceipt.events, "NewVersion");
Expand Down Expand Up @@ -319,7 +324,8 @@ async function publishRepoVersion(
pkg.dev,
pkg.flags,
version.version,
version.contentURIs
version.contentURIs,
["latest"]
);

// wait until the transaction is mined
Expand Down Expand Up @@ -348,10 +354,7 @@ async function assertRepoVersions(repo: Repo, expectedVersions: VersionStruct[])

for (let i = 1; i < versionCount + 1; i++) {
const version = await repo.getByVersionId(i);
versions.push({
version: version.version,
contentURIs: version.contentURIs,
});
versions.push(toVersion(version));
}

expect(versions).to.deep.equal(expectedVersions, "Wrong versions in repo");
Expand Down Expand Up @@ -387,3 +390,10 @@ function calculateFlagValue(visible: Boolean, active: Boolean, validated: Boolea
const value = Number(visible) + Number(active) * 2 + Number(validated) * 4 + Number(banned) * 8;
return ethers.BigNumber.from(value);
}

function toVersion(version: VersionStructOutput): VersionStruct {
return {
version: version.version,
contentURIs: version.contentURIs,
};
}