diff --git a/contracts/FleekERC721.sol b/contracts/FleekERC721.sol
index 956882d..14f4c2e 100644
--- a/contracts/FleekERC721.sol
+++ b/contracts/FleekERC721.sol
@@ -6,10 +6,13 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "./FleekAccessControl.sol";
+import "./util/FleekStrings.sol";
contract FleekERC721 is ERC721, FleekAccessControl {
- using Strings for uint256;
using Counters for Counters.Counter;
+ using FleekStrings for FleekERC721.App;
+ using FleekStrings for FleekERC721.AccessPoint;
+ using FleekStrings for string;
event NewBuild(uint256 indexed token, string indexed commitHash, address indexed triggeredBy);
event NewTokenName(uint256 indexed token, string indexed name, address indexed triggeredBy);
@@ -18,6 +21,27 @@ contract FleekERC721 is ERC721, FleekAccessControl {
event NewTokenExternalURL(uint256 indexed token, string indexed externalURL, address indexed triggeredBy);
event NewTokenENS(uint256 indexed token, string indexed ENS, address indexed triggeredBy);
+ event NewAccessPoint(string indexed apName, uint256 indexed tokenId, address indexed owner);
+ event RemoveAccessPoint(string indexed apName, uint256 indexed tokenId, address indexed owner);
+ event ChangeAccessPointScore(
+ string indexed apName,
+ uint256 indexed tokenId,
+ uint256 score,
+ address indexed triggeredBy
+ );
+ event ChangeAccessPointNameVerify(
+ string indexed apName,
+ uint256 tokenId,
+ bool indexed verified,
+ address indexed triggeredBy
+ );
+ event ChangeAccessPointContentVerify(
+ string indexed apName,
+ uint256 tokenId,
+ bool indexed verified,
+ address indexed triggeredBy
+ );
+
/**
* The properties are stored as string to keep consistency with
* other token contracts, we might consider changing for bytes32
@@ -30,6 +54,7 @@ contract FleekERC721 is ERC721, FleekAccessControl {
string ENS; // ENS ID
uint256 currentBuild; // The current build number (Increments by one with each change, starts at zero)
mapping(uint256 => Build) builds; // Mapping to build details for each build number
+ string[] accessPoints; // List of app AccessPoint
}
/**
@@ -40,8 +65,21 @@ contract FleekERC721 is ERC721, FleekAccessControl {
string gitRepository;
}
- Counters.Counter private _tokenIds;
+ /**
+ * The stored data for each AccessPoint.
+ */
+ struct AccessPoint {
+ uint256 tokenId;
+ uint256 index;
+ uint256 score;
+ bool contentVerified;
+ bool nameVerified;
+ address owner;
+ }
+
+ Counters.Counter private _appIds;
mapping(uint256 => App) private _apps;
+ mapping(string => AccessPoint) private _accessPoints;
/**
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
@@ -49,47 +87,13 @@ contract FleekERC721 is ERC721, FleekAccessControl {
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {}
/**
- * @dev Checks if msg.sender has the role of tokenOwner for a certain tokenId.
+ * @dev Checks if the AccessPoint exists.
*/
- modifier requireTokenOwner(uint256 tokenId) {
- require(msg.sender == ownerOf(tokenId), "FleekERC721: must be token owner");
+ modifier requireAP(string memory apName) {
+ require(_accessPoints[apName].owner != address(0), "FleekERC721: invalid AP");
_;
}
- /**
- * @dev Generates a SVG image.
- */
- function _generateSVG(string memory name, string memory ENS) internal view returns (string memory) {
- return (
- string(
- abi.encodePacked(
- _baseURI(),
- Base64.encode(
- abi.encodePacked(
- '"
- )
- )
- )
- )
- );
- }
-
/**
* @dev Mints a token and returns a tokenId.
*
@@ -109,9 +113,9 @@ contract FleekERC721 is ERC721, FleekAccessControl {
string memory commitHash,
string memory gitRepository
) public payable requireCollectionRole(Roles.Owner) returns (uint256) {
- uint256 tokenId = _tokenIds.current();
+ uint256 tokenId = _appIds.current();
_mint(to, tokenId);
- _tokenIds.increment();
+ _appIds.increment();
App storage app = _apps[tokenId];
app.name = name;
@@ -122,6 +126,7 @@ contract FleekERC721 is ERC721, FleekAccessControl {
// The mint interaction is considered to be the first build of the site. Updates from now on all increment the currentBuild by one and update the mapping.
app.currentBuild = 0;
app.builds[0] = Build(commitHash, gitRepository);
+ app.accessPoints = new string[](0);
return tokenId;
}
@@ -141,24 +146,7 @@ contract FleekERC721 is ERC721, FleekAccessControl {
address owner = ownerOf(tokenId);
App storage app = _apps[tokenId];
- // prettier-ignore
- bytes memory dataURI = abi.encodePacked(
- '{',
- '"name":"', app.name, '",',
- '"description":"', app.description, '",',
- '"owner":"', Strings.toHexString(uint160(owner), 20), '",',
- '"external_url":"', app.externalURL, '",',
- '"image":"', _generateSVG(app.name, app.ENS), '",',
- '"attributes": [',
- '{"trait_type": "ENS", "value":"', app.ENS,'"},',
- '{"trait_type": "Commit Hash", "value":"', app.builds[app.currentBuild].commitHash,'"},',
- '{"trait_type": "Repository", "value":"', app.builds[app.currentBuild].gitRepository,'"},',
- '{"trait_type": "Version", "value":"', Strings.toString(app.currentBuild),'"}',
- ']',
- '}'
- );
-
- return string(abi.encodePacked(_baseURI(), Base64.encode((dataURI))));
+ return string(abi.encodePacked(_baseURI(), app.toString(owner).toBase64()));
}
/**
@@ -278,6 +266,168 @@ contract FleekERC721 is ERC721, FleekAccessControl {
emit NewTokenDescription(tokenId, _tokenDescription, msg.sender);
}
+ /**
+ * @dev Add a new AccessPoint register for an app token.
+ * The AP name should be a DNS or ENS url and it should be unique.
+ * Anyone can add an AP but it should requires a payment.
+ *
+ * May emit a {NewAccessPoint} event.
+ *
+ * Requirements:
+ *
+ * - the tokenId must be minted and valid.
+ *
+ * IMPORTANT: The payment is not set yet
+ */
+ function addAccessPoint(uint256 tokenId, string memory apName) public payable {
+ // require(msg.value == 0.1 ether, "You need to pay at least 0.1 ETH"); // TODO: define a minimum price
+ _requireMinted(tokenId);
+ require(_accessPoints[apName].owner == address(0), "FleekERC721: AP already exists");
+
+ _accessPoints[apName] = AccessPoint(tokenId, _apps[tokenId].accessPoints.length, 0, false, false, msg.sender);
+ _apps[tokenId].accessPoints.push(apName);
+
+ emit NewAccessPoint(apName, tokenId, msg.sender);
+ }
+
+ /**
+ * @dev Remove an AccessPoint registry for an app token.
+ * It will also remove the AP from the app token APs list.
+ *
+ * May emit a {RemoveAccessPoint} event.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ * - must be called by the AP owner.
+ */
+ function removeAccessPoint(string memory apName) public requireAP(apName) {
+ require(msg.sender == _accessPoints[apName].owner, "FleekERC721: must be AP owner");
+ uint256 tokenId = _accessPoints[apName].tokenId;
+ App storage _app = _apps[tokenId];
+
+ // the index of the AP to remove
+ uint256 indexToRemove = _accessPoints[apName].index;
+
+ // the last item is reposited in the index to remove
+ string memory lastAP = _app.accessPoints[_app.accessPoints.length - 1];
+ _app.accessPoints[indexToRemove] = lastAP;
+ _accessPoints[lastAP].index = indexToRemove;
+
+ // remove the last item
+ _app.accessPoints.pop();
+
+ delete _accessPoints[apName];
+ emit RemoveAccessPoint(apName, tokenId, msg.sender);
+ }
+
+ /**
+ * @dev A view function to gether information about an AccessPoint.
+ * It returns a JSON string representing the AccessPoint information.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ *
+ */
+ function getAccessPointJSON(string memory apName) public view requireAP(apName) returns (string memory) {
+ AccessPoint storage _ap = _accessPoints[apName];
+ return _ap.toString();
+ }
+
+ /**
+ * @dev A view function to check if a AccessPoint is verified.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ *
+ */
+ function isAccessPointNameVerified(string memory apName) public view requireAP(apName) returns (bool) {
+ return _accessPoints[apName].nameVerified;
+ }
+
+ /**
+ * @dev Increases the score of a AccessPoint registry.
+ *
+ * May emit a {ChangeAccessPointScore} event.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ *
+ */
+ function increaseAccessPointScore(string memory apName) public requireAP(apName) {
+ _accessPoints[apName].score++;
+ emit ChangeAccessPointScore(apName, _accessPoints[apName].tokenId, _accessPoints[apName].score, msg.sender);
+ }
+
+ /**
+ * @dev Decreases the score of a AccessPoint registry if is greater than 0.
+ *
+ * May emit a {ChangeAccessPointScore} event.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ *
+ */
+ function decreaseAccessPointScore(string memory apName) public requireAP(apName) {
+ require(_accessPoints[apName].score > 0, "FleekERC721: score cant be lower");
+ _accessPoints[apName].score--;
+ emit ChangeAccessPointScore(apName, _accessPoints[apName].tokenId, _accessPoints[apName].score, msg.sender);
+ }
+
+ /**
+ * @dev Set the content verification of a AccessPoint registry.
+ *
+ * May emit a {ChangeAccessPointContentVerify} event.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ * - the sender must have the token controller role.
+ *
+ */
+ function setAccessPointContentVerify(
+ string memory apName,
+ bool verified
+ ) public requireAP(apName) requireTokenRole(_accessPoints[apName].tokenId, Roles.Controller) {
+ _accessPoints[apName].contentVerified = verified;
+ emit ChangeAccessPointContentVerify(apName, _accessPoints[apName].tokenId, verified, msg.sender);
+ }
+
+ /**
+ * @dev Set the name verification of a AccessPoint registry.
+ *
+ * May emit a {ChangeAccessPointNameVerify} event.
+ *
+ * Requirements:
+ *
+ * - the AP must exist.
+ * - the sender must have the token controller role.
+ *
+ */
+ function setAccessPointNameVerify(
+ string memory apName,
+ bool verified
+ ) public requireAP(apName) requireTokenRole(_accessPoints[apName].tokenId, Roles.Controller) {
+ _accessPoints[apName].nameVerified = verified;
+ emit ChangeAccessPointNameVerify(apName, _accessPoints[apName].tokenId, verified, msg.sender);
+ }
+
+ /**
+ * @dev A view function to gether the list of mirrros for a given app.
+ *
+ * Requirements:
+ * - the tokenId must be minted and valid.
+ *
+ */
+ function appAccessPoints(uint256 tokenId) public view returns (string[] memory) {
+ _requireMinted(tokenId);
+ return _apps[tokenId].accessPoints;
+ }
+
/**
* @dev Adds a new build to a minted `tokenId`'s builds mapping.
*
diff --git a/contracts/util/FleekSVG.sol b/contracts/util/FleekSVG.sol
new file mode 100644
index 0000000..9cac338
--- /dev/null
+++ b/contracts/util/FleekSVG.sol
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.7;
+
+import "../FleekERC721.sol";
+import "@openzeppelin/contracts/utils/Strings.sol";
+import "@openzeppelin/contracts/utils/Base64.sol";
+
+library FleekSVG {
+ /**
+ * @dev Generates a SVG image.
+ */
+ function generateBase64(string memory name, string memory ENS) internal pure returns (string memory) {
+ return (
+ string(
+ abi.encodePacked(
+ "data:application/json;base64,",
+ Base64.encode(
+ abi.encodePacked(
+ '"
+ )
+ )
+ )
+ )
+ );
+ }
+}
diff --git a/contracts/util/FleekStrings.sol b/contracts/util/FleekStrings.sol
new file mode 100644
index 0000000..b34bba5
--- /dev/null
+++ b/contracts/util/FleekStrings.sol
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.7;
+
+import "../FleekERC721.sol";
+import "@openzeppelin/contracts/utils/Strings.sol";
+import "@openzeppelin/contracts/utils/Base64.sol";
+import "./FleekSVG.sol";
+
+library FleekStrings {
+ using Strings for uint256;
+ using Strings for uint160;
+ using FleekStrings for bool;
+
+ /**
+ * @dev Converts a boolean value to a string.
+ */
+ function toString(bool _bool) internal pure returns (string memory) {
+ return _bool ? "true" : "false";
+ }
+
+ /**
+ * @dev Converts a string to a base64 string.
+ */
+ function toBase64(string memory str) internal pure returns (string memory) {
+ return Base64.encode(bytes(str));
+ }
+
+ /**
+ * @dev Converts FleekERC721.App to a JSON string.
+ * It requires to receive owner address as a parameter.
+ */
+ function toString(FleekERC721.App storage app, address owner) internal view returns (string memory) {
+ // prettier-ignore
+ return string(abi.encodePacked(
+ '{',
+ '"name":"', app.name, '",',
+ '"description":"', app.description, '",',
+ '"owner":"', uint160(owner).toHexString(20), '",',
+ '"external_url":"', app.externalURL, '",',
+ '"image":"', FleekSVG.generateBase64(app.name, app.ENS), '",',
+ '"attributes": [',
+ '{"trait_type": "ENS", "value":"', app.ENS,'"},',
+ '{"trait_type": "Commit Hash", "value":"', app.builds[app.currentBuild].commitHash,'"},',
+ '{"trait_type": "Repository", "value":"', app.builds[app.currentBuild].gitRepository,'"},',
+ '{"trait_type": "Version", "value":"', app.currentBuild.toString(),'"}',
+ ']',
+ '}'
+ ));
+ }
+
+ /**
+ * @dev Converts FleekERC721.AccessPoint to a JSON string.
+ */
+ function toString(FleekERC721.AccessPoint storage ap) internal view returns (string memory) {
+ // prettier-ignore
+ return string(abi.encodePacked(
+ "{",
+ '"tokenId":', ap.tokenId.toString(), ",",
+ '"score":', ap.score.toString(), ",",
+ '"nameVerified":', ap.nameVerified.toString(), ",",
+ '"contentVerified":', ap.contentVerified.toString(), ",",
+ '"owner":"', uint160(ap.owner).toHexString(20), '"',
+ "}"
+ ));
+ }
+}
diff --git a/hardhat.config.ts b/hardhat.config.ts
index d8bf1ac..1f9060d 100644
--- a/hardhat.config.ts
+++ b/hardhat.config.ts
@@ -52,16 +52,16 @@ const config: HardhatUserConfig = {
},
},
solidity: {
- version: '0.8.7',
+ version: '0.8.12',
settings: {
optimizer: {
enabled: true,
runs: 200,
details: {
- yul: false,
+ yul: true,
},
},
- viaIR: false,
+ viaIR: true,
},
},
mocha: {
diff --git a/test/FleekERC721.ts b/test/FleekERC721.ts
index 482763a..d59a391 100644
--- a/test/FleekERC721.ts
+++ b/test/FleekERC721.ts
@@ -600,4 +600,244 @@ describe('FleekERC721', () => {
.withArgs(ROLES.CONTROLLER, otherAccount.address, owner.address);
});
});
+
+ describe('AccessPoints', () => {
+ let tokenId: number;
+ let fixture: Awaited>;
+
+ const getDefaultAddParams = () => [tokenId, 'accesspoint.com'];
+
+ beforeEach(async () => {
+ fixture = await loadFixture(defaultFixture);
+ const { contract } = fixture;
+
+ const response = await contract.mint(
+ fixture.owner.address,
+ MINT_PARAMS.name,
+ MINT_PARAMS.description,
+ MINT_PARAMS.externalUrl,
+ MINT_PARAMS.ens,
+ MINT_PARAMS.commitHash,
+ MINT_PARAMS.gitRepository
+ );
+
+ tokenId = response.value.toNumber();
+ });
+
+ it('should add an AP', async () => {
+ const { contract, owner } = fixture;
+
+ await expect(contract.addAccessPoint(...getDefaultAddParams()))
+ .to.emit(contract, 'NewAccessPoint')
+ .withArgs('accesspoint.com', tokenId, owner.address);
+
+ expect(await contract.appAccessPoints(tokenId)).eql(['accesspoint.com']);
+ });
+
+ it('should return a AP json object', async () => {
+ const { contract, owner } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp).to.eql({
+ tokenId,
+ score: 0,
+ owner: owner.address.toLowerCase(),
+ contentVerified: false,
+ nameVerified: false,
+ });
+ });
+
+ it('should revert if AP does not exist', async () => {
+ const { contract } = fixture;
+
+ await expect(
+ contract.getAccessPointJSON('accesspoint.com')
+ ).to.be.revertedWith('FleekERC721: invalid AP');
+ });
+
+ it('should increase the AP score', async () => {
+ const { contract, owner } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await contract.increaseAccessPointScore('accesspoint.com');
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp).to.eql({
+ tokenId,
+ score: 1,
+ owner: owner.address.toLowerCase(),
+ contentVerified: false,
+ nameVerified: false,
+ });
+ });
+
+ it('should decrease the AP score', async () => {
+ const { contract, owner } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await contract.increaseAccessPointScore('accesspoint.com');
+ await contract.increaseAccessPointScore('accesspoint.com');
+ await contract.decreaseAccessPointScore('accesspoint.com');
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp).to.eql({
+ tokenId,
+ score: 1,
+ owner: owner.address.toLowerCase(),
+ contentVerified: false,
+ nameVerified: false,
+ });
+ });
+
+ it('should allow anyone to change AP score', async () => {
+ const { contract, otherAccount } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+ await contract.increaseAccessPointScore('accesspoint.com');
+ await contract
+ .connect(otherAccount)
+ .increaseAccessPointScore('accesspoint.com');
+ });
+
+ it('should remove an AP', async () => {
+ const { contract, owner } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await expect(contract.removeAccessPoint('accesspoint.com'))
+ .to.emit(contract, 'RemoveAccessPoint')
+ .withArgs('accesspoint.com', tokenId, owner.address);
+
+ expect(await contract.appAccessPoints(tokenId)).eql([]);
+ });
+
+ it('should allow only AP owner to remove it', async () => {
+ const { contract, otherAccount } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await expect(
+ contract.connect(otherAccount).removeAccessPoint('accesspoint.com')
+ ).to.be.revertedWith('FleekERC721: must be AP owner');
+ });
+
+ it('should not be allowed to add the same AP more than once', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await expect(
+ contract.addAccessPoint(...getDefaultAddParams())
+ ).to.be.revertedWith('FleekERC721: AP already exists');
+ });
+
+ it('should change "contentVerified" to true', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await contract.setAccessPointContentVerify('accesspoint.com', true);
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp.contentVerified).to.be.true;
+ });
+
+ it('should change "contentVerified" to false', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ const beforeAp = await contract.getAccessPointJSON('accesspoint.com');
+ const beforeParsedAp = JSON.parse(beforeAp);
+ expect(beforeParsedAp.contentVerified).to.be.false;
+
+ await contract.setAccessPointContentVerify('accesspoint.com', true);
+ await contract.setAccessPointContentVerify('accesspoint.com', false);
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp.contentVerified).to.be.false;
+ });
+
+ it('should change "nameVerified" to true', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ await contract.setAccessPointNameVerify('accesspoint.com', true);
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp.nameVerified).to.be.true;
+ });
+
+ it('should change "nameVerified" to false', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(...getDefaultAddParams());
+
+ const beforeAp = await contract.getAccessPointJSON('accesspoint.com');
+ const beforeParsedAp = JSON.parse(beforeAp);
+ expect(beforeParsedAp.nameVerified).to.be.false;
+
+ await contract.setAccessPointNameVerify('accesspoint.com', true);
+ await contract.setAccessPointNameVerify('accesspoint.com', false);
+
+ const ap = await contract.getAccessPointJSON('accesspoint.com');
+ const parsedAp = JSON.parse(ap);
+
+ expect(parsedAp.nameVerified).to.be.false;
+ });
+
+ it('should get a list of added APs for an app', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(tokenId, 'accesspoint1.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint2.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint3.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint4.com');
+
+ const aps = await contract.appAccessPoints(tokenId);
+
+ expect(aps).to.eql([
+ 'accesspoint1.com',
+ 'accesspoint2.com',
+ 'accesspoint3.com',
+ 'accesspoint4.com',
+ ]);
+ });
+
+ it('should get a list of added APs for an app after removing one', async () => {
+ const { contract } = fixture;
+
+ await contract.addAccessPoint(tokenId, 'accesspoint1.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint2.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint3.com');
+ await contract.addAccessPoint(tokenId, 'accesspoint4.com');
+
+ await contract.removeAccessPoint('accesspoint2.com');
+
+ const aps = await contract.appAccessPoints(tokenId);
+
+ expect(aps).to.eql([
+ 'accesspoint1.com',
+ 'accesspoint4.com',
+ 'accesspoint3.com',
+ ]);
+ });
+ });
});