From d03e4d6ffe4164d7f060bb7770e1996c91c75982 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 2 Oct 2024 17:21:20 +0200 Subject: [PATCH 1/7] chore(XPNFTToken): add base XPNFTToken --- src/XPNFTToken.sol | 128 +++++++++++++++++++++++++++++++++++ test/RewardsStreamer.t.sol | 2 +- test/RewardsStreamerMP.t.sol | 3 +- test/XPNFTToken.t.sol | 32 +++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 src/XPNFTToken.sol create mode 100644 test/XPNFTToken.t.sol diff --git a/src/XPNFTToken.sol b/src/XPNFTToken.sol new file mode 100644 index 0000000..1e363f7 --- /dev/null +++ b/src/XPNFTToken.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); +} + +contract XPNFTToken is Ownable { + error XPNFT__TransferNotAllowed(); + error XPNFT__InvalidTokenId(); + + string private _name = "XPNFT"; + string private _symbol = "XPNFT"; + string private _imagePrefix = ""; + string private _imageSuffix = ""; + + IERC20 private _xpToken; + + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + + modifier onlyValidTokenId(uint256 tokenId) { + if (tokenId > type(uint160).max) { + revert XPNFT__InvalidTokenId(); + } + _; + } + + constructor(address xpTokenAddress, string memory imagePrefix, string memory imageSuffix) Ownable(msg.sender) { + _xpToken = IERC20(xpTokenAddress); + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function name() external view returns (string memory) { + return _name; + } + + function symbol() external view returns (string memory) { + return _symbol; + } + + function mint() external { + emit Transfer(msg.sender, msg.sender, uint256(uint160(msg.sender))); + } + + function balanceOf(address) external pure returns (uint256) { + return 1; + } + + function ownerOf(uint256 tokenId) external pure onlyValidTokenId(tokenId) returns (address) { + address owner = address(uint160(tokenId)); + return owner; + } + + function safeTransferFrom(address, address, uint256, bytes calldata) external pure { + revert XPNFT__TransferNotAllowed(); + } + + function safeTransferFrom(address, address, uint256) external pure { + revert XPNFT__TransferNotAllowed(); + } + + function transferFrom(address, address, uint256) external pure { + revert XPNFT__TransferNotAllowed(); + } + + function approve(address, uint256) external pure { + revert XPNFT__TransferNotAllowed(); + } + + function setApprovalForAll(address, bool) external pure { + revert XPNFT__TransferNotAllowed(); + } + + function getApproved(uint256) external pure returns (address) { + return address(0); + } + + function isApprovedForAll(address, address) external pure returns (bool) { + return false; + } + + function tokenURI(uint256 tokenId) external view onlyValidTokenId(tokenId) returns (string memory) { + address owner = address(uint160(tokenId)); + return _createTokenURI(owner); + } + + function _createTokenURI(address owner) internal view returns (string memory) { + string memory baseName = "XPNFT Token "; + string memory baseDescription = "This is a XPNFT token for address "; + uint256 balance = _xpToken.balanceOf(owner) / 1e18; + + string memory propName = string(abi.encodePacked(baseName, Strings.toHexString(owner))); + string memory propDescription = string( + abi.encodePacked(baseDescription, Strings.toHexString(owner), " with balance ", Strings.toString(balance)) + ); + string memory image = _generateImage(balance); + + bytes memory json = abi.encodePacked( + "{\"name\":\"", + propName, + "\",\"description\":\"", + propDescription, + "\",\"image\":\"data:image/svg+xml;base64,", + image, + "\"}" + ); + + string memory jsonBase64 = Base64.encode(json); + return string(abi.encodePacked("data:application/json;base64,", jsonBase64)); + } + + function _generateImage(uint256 balance) internal view returns (string memory) { + string memory text = Strings.toString(balance); + bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); + + return Base64.encode(svg); + } +} diff --git a/test/RewardsStreamer.t.sol b/test/RewardsStreamer.t.sol index 9ed3a2c..32c15a1 100644 --- a/test/RewardsStreamer.t.sol +++ b/test/RewardsStreamer.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { RewardsStreamer } from "../src/RewardsStreamer.sol"; import { MockToken } from "./mocks/MockToken.sol"; diff --git a/test/RewardsStreamerMP.t.sol b/test/RewardsStreamerMP.t.sol index ee3c4e7..d83ecfc 100644 --- a/test/RewardsStreamerMP.t.sol +++ b/test/RewardsStreamerMP.t.sol @@ -1,10 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { RewardsStreamerMP } from "../src/RewardsStreamerMP.sol"; import { MockToken } from "./mocks/MockToken.sol"; -import "forge-std/console.sol"; contract RewardsStreamerMPTest is Test { MockToken rewardToken; diff --git a/test/XPNFTToken.t.sol b/test/XPNFTToken.t.sol new file mode 100644 index 0000000..b371c9c --- /dev/null +++ b/test/XPNFTToken.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Test, console } from "forge-std/Test.sol"; +import { MockToken } from "./mocks/MockToken.sol"; +import { XPNFTToken } from "../src/XPNFTToken.sol"; + +contract XPNFTTokenTest is Test { + MockToken erc20Token; + XPNFTToken nft; + + address alice = makeAddr("alice"); + + string imagePrefix = + ''; + string imageSuffix = ""; + + function setUp() public { + erc20Token = new MockToken("Test", "TEST"); + nft = new XPNFTToken(address(erc20Token), imagePrefix, imageSuffix); + + address[1] memory users = [alice]; + for (uint256 i = 0; i < users.length; i++) { + erc20Token.mint(users[i], 10e18); + } + } + + function test() public { + string memory metadata = nft.tokenURI(uint256(uint160(alice))); + console.log(metadata); + } +} From fe0c64941efecb3a451e6429669a6ee5b7eb1ecc Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Fri, 4 Oct 2024 15:18:53 +0200 Subject: [PATCH 2/7] feature: add XPNFTMetadataGenerator --- src/XPNFTMetadataGenerator.sol | 59 +++++++++++++++++++++++++++++ src/XPNFTToken.sol | 69 +++++++--------------------------- test/XPNFTToken.t.sol | 5 ++- 3 files changed, 76 insertions(+), 57 deletions(-) create mode 100644 src/XPNFTMetadataGenerator.sol diff --git a/src/XPNFTMetadataGenerator.sol b/src/XPNFTMetadataGenerator.sol new file mode 100644 index 0000000..4619ea0 --- /dev/null +++ b/src/XPNFTMetadataGenerator.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +interface IMetadataGenerator { + function generate(address account, uint256 balance) external view returns (string memory); +} + +contract XPNFTMetadataGenerator is IMetadataGenerator, Ownable { + address public nft; + + string private _imagePrefix = ""; + string private _imageSuffix = ""; + + constructor(address _nft, string memory imagePrefix, string memory imageSuffix) Ownable(msg.sender) { + nft = _nft; + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function generate(address account, uint256 balance) external view returns (string memory) { + string memory baseName = "XPNFT Token "; + string memory baseDescription = "This is a XPNFT token for address "; + + string memory propName = string(abi.encodePacked(baseName, Strings.toHexString(account))); + string memory propDescription = string( + abi.encodePacked(baseDescription, Strings.toHexString(account), " with balance ", Strings.toString(balance)) + ); + string memory image = _generateImage(balance); + + bytes memory json = abi.encodePacked( + "{\"name\":\"", + propName, + "\",\"description\":\"", + propDescription, + "\",\"image\":\"data:image/svg+xml;base64,", + image, + "\"}" + ); + + string memory jsonBase64 = Base64.encode(json); + return string(abi.encodePacked("data:application/json;base64,", jsonBase64)); + } + + function _generateImage(uint256 balance) internal view returns (string memory) { + string memory text = Strings.toString(balance / 10e18); + bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); + + return Base64.encode(svg); + } +} diff --git a/src/XPNFTToken.sol b/src/XPNFTToken.sol index 1e363f7..d8be252 100644 --- a/src/XPNFTToken.sol +++ b/src/XPNFTToken.sol @@ -1,9 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IMetadataGenerator } from "./XPNFTMetadataGenerator.sol"; interface IERC20 { function balanceOf(address account) external view returns (uint256); @@ -13,12 +12,11 @@ contract XPNFTToken is Ownable { error XPNFT__TransferNotAllowed(); error XPNFT__InvalidTokenId(); - string private _name = "XPNFT"; - string private _symbol = "XPNFT"; - string private _imagePrefix = ""; - string private _imageSuffix = ""; + IERC20 public xpToken; + IMetadataGenerator public metadataGenerator; - IERC20 private _xpToken; + string private name = "XPNFT"; + string private symbol = "XPNFT"; event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); @@ -29,23 +27,13 @@ contract XPNFTToken is Ownable { _; } - constructor(address xpTokenAddress, string memory imagePrefix, string memory imageSuffix) Ownable(msg.sender) { - _xpToken = IERC20(xpTokenAddress); - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; + constructor(address xpTokenAddress, address _metadataGenerator) Ownable(msg.sender) { + xpToken = IERC20(xpTokenAddress); + metadataGenerator = IMetadataGenerator(_metadataGenerator); } - function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; - } - - function name() external view returns (string memory) { - return _name; - } - - function symbol() external view returns (string memory) { - return _symbol; + function setMetadataGenerator(address _metadataGenerator) external onlyOwner { + metadataGenerator = IMetadataGenerator(_metadataGenerator); } function mint() external { @@ -90,39 +78,8 @@ contract XPNFTToken is Ownable { } function tokenURI(uint256 tokenId) external view onlyValidTokenId(tokenId) returns (string memory) { - address owner = address(uint160(tokenId)); - return _createTokenURI(owner); - } - - function _createTokenURI(address owner) internal view returns (string memory) { - string memory baseName = "XPNFT Token "; - string memory baseDescription = "This is a XPNFT token for address "; - uint256 balance = _xpToken.balanceOf(owner) / 1e18; - - string memory propName = string(abi.encodePacked(baseName, Strings.toHexString(owner))); - string memory propDescription = string( - abi.encodePacked(baseDescription, Strings.toHexString(owner), " with balance ", Strings.toString(balance)) - ); - string memory image = _generateImage(balance); - - bytes memory json = abi.encodePacked( - "{\"name\":\"", - propName, - "\",\"description\":\"", - propDescription, - "\",\"image\":\"data:image/svg+xml;base64,", - image, - "\"}" - ); - - string memory jsonBase64 = Base64.encode(json); - return string(abi.encodePacked("data:application/json;base64,", jsonBase64)); - } - - function _generateImage(uint256 balance) internal view returns (string memory) { - string memory text = Strings.toString(balance); - bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); - - return Base64.encode(svg); + address account = address(uint160(tokenId)); + uint256 balance = xpToken.balanceOf(account); + return metadataGenerator.generate(account, balance); } } diff --git a/test/XPNFTToken.t.sol b/test/XPNFTToken.t.sol index b371c9c..6290b0b 100644 --- a/test/XPNFTToken.t.sol +++ b/test/XPNFTToken.t.sol @@ -4,9 +4,11 @@ pragma solidity ^0.8.26; import { Test, console } from "forge-std/Test.sol"; import { MockToken } from "./mocks/MockToken.sol"; import { XPNFTToken } from "../src/XPNFTToken.sol"; +import { XPNFTMetadataGenerator } from "../src/XPNFTMetadataGenerator.sol"; contract XPNFTTokenTest is Test { MockToken erc20Token; + XPNFTMetadataGenerator metadataGenerator; XPNFTToken nft; address alice = makeAddr("alice"); @@ -17,7 +19,8 @@ contract XPNFTTokenTest is Test { function setUp() public { erc20Token = new MockToken("Test", "TEST"); - nft = new XPNFTToken(address(erc20Token), imagePrefix, imageSuffix); + metadataGenerator = new XPNFTMetadataGenerator(address(erc20Token), imagePrefix, imageSuffix); + nft = new XPNFTToken(address(erc20Token), address(metadataGenerator)); address[1] memory users = [alice]; for (uint256 i = 0; i < users.length; i++) { From 724781262efa58141cba76c6ee1d2ea49031e3f1 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 9 Oct 2024 12:49:23 +0200 Subject: [PATCH 3/7] chore(XPNFTToken): add INFTMetadataGenerator with Base and SVG generator --- src/XPNFTMetadataGenerator.sol | 59 ------------------- src/XPNFTToken.sol | 8 +-- src/interfaces/INFTMetadataGenerator.sol | 6 ++ .../BaseNFTMetadataGenerator.sol | 36 +++++++++++ .../NFTMetadataGeneratorSVG.sol | 29 +++++++++ test/XPNFTToken.t.sol | 23 +++++--- test/mocks/MockMetadataGenerator.sol | 18 ++++++ 7 files changed, 108 insertions(+), 71 deletions(-) delete mode 100644 src/XPNFTMetadataGenerator.sol create mode 100644 src/interfaces/INFTMetadataGenerator.sol create mode 100644 src/nft-metadata-generators/BaseNFTMetadataGenerator.sol create mode 100644 src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol create mode 100644 test/mocks/MockMetadataGenerator.sol diff --git a/src/XPNFTMetadataGenerator.sol b/src/XPNFTMetadataGenerator.sol deleted file mode 100644 index 4619ea0..0000000 --- a/src/XPNFTMetadataGenerator.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.26; - -import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -interface IMetadataGenerator { - function generate(address account, uint256 balance) external view returns (string memory); -} - -contract XPNFTMetadataGenerator is IMetadataGenerator, Ownable { - address public nft; - - string private _imagePrefix = ""; - string private _imageSuffix = ""; - - constructor(address _nft, string memory imagePrefix, string memory imageSuffix) Ownable(msg.sender) { - nft = _nft; - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; - } - - function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; - } - - function generate(address account, uint256 balance) external view returns (string memory) { - string memory baseName = "XPNFT Token "; - string memory baseDescription = "This is a XPNFT token for address "; - - string memory propName = string(abi.encodePacked(baseName, Strings.toHexString(account))); - string memory propDescription = string( - abi.encodePacked(baseDescription, Strings.toHexString(account), " with balance ", Strings.toString(balance)) - ); - string memory image = _generateImage(balance); - - bytes memory json = abi.encodePacked( - "{\"name\":\"", - propName, - "\",\"description\":\"", - propDescription, - "\",\"image\":\"data:image/svg+xml;base64,", - image, - "\"}" - ); - - string memory jsonBase64 = Base64.encode(json); - return string(abi.encodePacked("data:application/json;base64,", jsonBase64)); - } - - function _generateImage(uint256 balance) internal view returns (string memory) { - string memory text = Strings.toString(balance / 10e18); - bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); - - return Base64.encode(svg); - } -} diff --git a/src/XPNFTToken.sol b/src/XPNFTToken.sol index d8be252..9240441 100644 --- a/src/XPNFTToken.sol +++ b/src/XPNFTToken.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { IMetadataGenerator } from "./XPNFTMetadataGenerator.sol"; +import { INFTMetadataGenerator } from "./interfaces/INFTMetadataGenerator.sol"; interface IERC20 { function balanceOf(address account) external view returns (uint256); @@ -13,7 +13,7 @@ contract XPNFTToken is Ownable { error XPNFT__InvalidTokenId(); IERC20 public xpToken; - IMetadataGenerator public metadataGenerator; + INFTMetadataGenerator public metadataGenerator; string private name = "XPNFT"; string private symbol = "XPNFT"; @@ -29,11 +29,11 @@ contract XPNFTToken is Ownable { constructor(address xpTokenAddress, address _metadataGenerator) Ownable(msg.sender) { xpToken = IERC20(xpTokenAddress); - metadataGenerator = IMetadataGenerator(_metadataGenerator); + metadataGenerator = INFTMetadataGenerator(_metadataGenerator); } function setMetadataGenerator(address _metadataGenerator) external onlyOwner { - metadataGenerator = IMetadataGenerator(_metadataGenerator); + metadataGenerator = INFTMetadataGenerator(_metadataGenerator); } function mint() external { diff --git a/src/interfaces/INFTMetadataGenerator.sol b/src/interfaces/INFTMetadataGenerator.sol new file mode 100644 index 0000000..25701aa --- /dev/null +++ b/src/interfaces/INFTMetadataGenerator.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +interface INFTMetadataGenerator { + function generate(address account, uint256 balance) external view returns (string memory); +} diff --git a/src/nft-metadata-generators/BaseNFTMetadataGenerator.sol b/src/nft-metadata-generators/BaseNFTMetadataGenerator.sol new file mode 100644 index 0000000..60b115f --- /dev/null +++ b/src/nft-metadata-generators/BaseNFTMetadataGenerator.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { INFTMetadataGenerator } from "../interfaces/INFTMetadataGenerator.sol"; + +abstract contract BaseNFTMetadataGenerator is INFTMetadataGenerator, Ownable { + address public nft; + + constructor(address _nft) Ownable(msg.sender) { + nft = _nft; + } + + function generate(address account, uint256 balance) external view returns (string memory) { + string memory baseName = "XPNFT Token "; + string memory baseDescription = "This is a XPNFT token for address "; + + string memory propName = string(abi.encodePacked(baseName, Strings.toHexString(account))); + string memory propDescription = string( + abi.encodePacked(baseDescription, Strings.toHexString(account), " with balance ", Strings.toString(balance)) + ); + + string memory image = generateImageURI(account, balance); + + bytes memory json = abi.encodePacked( + "{\"name\":\"", propName, "\",\"description\":\"", propDescription, "\",\"image\":\"", image, "\"}" + ); + + string memory jsonBase64 = Base64.encode(json); + return string(abi.encodePacked("data:application/json;base64,", jsonBase64)); + } + + function generateImageURI(address account, uint256 balance) internal view virtual returns (string memory); +} diff --git a/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol new file mode 100644 index 0000000..f5c9496 --- /dev/null +++ b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { BaseNFTMetadataGenerator } from "./BaseNFTMetadataGenerator.sol"; + +contract NFTMetadataGeneratorSVG is BaseNFTMetadataGenerator { + string private _imagePrefix = ""; + string private _imageSuffix = ""; + + constructor(address nft, string memory imagePrefix, string memory imageSuffix) BaseNFTMetadataGenerator(nft) { + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { + _imagePrefix = imagePrefix; + _imageSuffix = imageSuffix; + } + + function generateImageURI(address, uint256 balance) internal view override returns (string memory) { + string memory text = Strings.toString(balance / 10e18); + bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); + + return Base64.encode(svg); + } +} diff --git a/test/XPNFTToken.t.sol b/test/XPNFTToken.t.sol index 6290b0b..5f65c4d 100644 --- a/test/XPNFTToken.t.sol +++ b/test/XPNFTToken.t.sol @@ -2,24 +2,26 @@ pragma solidity ^0.8.26; import { Test, console } from "forge-std/Test.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { MockToken } from "./mocks/MockToken.sol"; import { XPNFTToken } from "../src/XPNFTToken.sol"; -import { XPNFTMetadataGenerator } from "../src/XPNFTMetadataGenerator.sol"; +import { MockMetadataGenerator } from "./mocks/MockMetadataGenerator.sol"; contract XPNFTTokenTest is Test { MockToken erc20Token; - XPNFTMetadataGenerator metadataGenerator; + MockMetadataGenerator metadataGenerator; XPNFTToken nft; address alice = makeAddr("alice"); - string imagePrefix = - ''; - string imageSuffix = ""; + // string imagePrefix = + // 'data:image/svg+xml;base64,'; + // string imageSuffix = ""; function setUp() public { erc20Token = new MockToken("Test", "TEST"); - metadataGenerator = new XPNFTMetadataGenerator(address(erc20Token), imagePrefix, imageSuffix); + metadataGenerator = new MockMetadataGenerator(address(erc20Token), "https://test.local/"); nft = new XPNFTToken(address(erc20Token), address(metadataGenerator)); address[1] memory users = [alice]; @@ -28,8 +30,13 @@ contract XPNFTTokenTest is Test { } } - function test() public { + function test() public view { + bytes memory expectedMetadata = abi.encodePacked( + '{"name":"XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6",', + '"description":"This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000",', + '"image":"https://test.local/0x328809bc894f92807417d2dad6b7c998c1afdac6"}' + ); string memory metadata = nft.tokenURI(uint256(uint160(alice))); - console.log(metadata); + assertEq(metadata, string(abi.encodePacked("data:application/json;base64,", Base64.encode(expectedMetadata)))); } } diff --git a/test/mocks/MockMetadataGenerator.sol b/test/mocks/MockMetadataGenerator.sol new file mode 100644 index 0000000..cbaed9a --- /dev/null +++ b/test/mocks/MockMetadataGenerator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { BaseNFTMetadataGenerator } from "../../src/nft-metadata-generators/BaseNFTMetadataGenerator.sol"; + +contract MockMetadataGenerator is BaseNFTMetadataGenerator { + string private _baseURI; + + constructor(address nft, string memory baseURI) BaseNFTMetadataGenerator(nft) { + _baseURI = baseURI; + } + + function generateImageURI(address account, uint256) internal view override returns (string memory) { + bytes memory uri = abi.encodePacked(_baseURI, Strings.toHexString(account)); + return string(uri); + } +} From 84a547ddd5ebad4293727b60cd72ff0b2e3ee7a2 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 9 Oct 2024 13:28:12 +0200 Subject: [PATCH 4/7] chore(XPNFTToken): add tests --- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- test/XPNFTToken.t.sol | 73 ++++++++++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index 1714bee..035de35 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 035de35f5e366c8d6ed142aec4ccb57fe2dd87d4 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dbb6104..6325009 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 +Subproject commit 632500967504310a07f9d2c70ad378cf53be0109 diff --git a/test/XPNFTToken.t.sol b/test/XPNFTToken.t.sol index 5f65c4d..c0c5b57 100644 --- a/test/XPNFTToken.t.sol +++ b/test/XPNFTToken.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.26; import { Test, console } from "forge-std/Test.sol"; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { MockToken } from "./mocks/MockToken.sol"; import { XPNFTToken } from "../src/XPNFTToken.sol"; import { MockMetadataGenerator } from "./mocks/MockMetadataGenerator.sol"; @@ -14,11 +15,6 @@ contract XPNFTTokenTest is Test { address alice = makeAddr("alice"); - // string imagePrefix = - // 'data:image/svg+xml;base64,'; - // string imageSuffix = ""; - function setUp() public { erc20Token = new MockToken("Test", "TEST"); metadataGenerator = new MockMetadataGenerator(address(erc20Token), "https://test.local/"); @@ -30,13 +26,70 @@ contract XPNFTTokenTest is Test { } } - function test() public view { + function addressToId(address addr) internal pure returns (uint256) { + return uint256(uint160(addr)); + } + + function testTokenURI() public view { bytes memory expectedMetadata = abi.encodePacked( - '{"name":"XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6",', - '"description":"This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000",', - '"image":"https://test.local/0x328809bc894f92807417d2dad6b7c998c1afdac6"}' + "{\"name\":\"XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6\",", + "\"description\":\"This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000\",", + "\"image\":\"https://test.local/0x328809bc894f92807417d2dad6b7c998c1afdac6\"}" ); - string memory metadata = nft.tokenURI(uint256(uint160(alice))); + string memory metadata = nft.tokenURI(addressToId(alice)); assertEq(metadata, string(abi.encodePacked("data:application/json;base64,", Base64.encode(expectedMetadata)))); } + + function testSetMetadataGenerator() public { + MockMetadataGenerator newMetadataGenerator = + new MockMetadataGenerator(address(erc20Token), "https://new-test.local/"); + + nft.setMetadataGenerator(address(newMetadataGenerator)); + + assertEq(address(nft.metadataGenerator()), address(newMetadataGenerator)); + } + + function testSetMetadataGeneratorRevert() public { + MockMetadataGenerator newMetadataGenerator = + new MockMetadataGenerator(address(erc20Token), "https://new-test.local/"); + + vm.prank(alice); + vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); + nft.setMetadataGenerator(address(newMetadataGenerator)); + } + + function testTransferNotAllowed() public { + vm.expectRevert(XPNFTToken.XPNFT__TransferNotAllowed.selector); + nft.transferFrom(alice, address(0), addressToId(alice)); + } + + function testSafeTransferNotAllowed() public { + vm.expectRevert(XPNFTToken.XPNFT__TransferNotAllowed.selector); + nft.safeTransferFrom(alice, address(0), addressToId(alice)); + } + + function testSafeTransferWithDataNotAllowed() public { + vm.expectRevert(XPNFTToken.XPNFT__TransferNotAllowed.selector); + nft.safeTransferFrom(alice, address(0), addressToId(alice), ""); + } + + function testApproveNotAllowed() public { + vm.expectRevert(XPNFTToken.XPNFT__TransferNotAllowed.selector); + nft.approve(address(0), addressToId(alice)); + } + + function testSetApprovalForAllNotAllowed() public { + vm.expectRevert(XPNFTToken.XPNFT__TransferNotAllowed.selector); + nft.setApprovalForAll(address(0), true); + } + + function testGetApproved() public view { + address approved = nft.getApproved(addressToId(alice)); + assertEq(approved, address(0)); + } + + function testIsApprovedForAll() public view { + bool isApproved = nft.isApprovedForAll(alice, address(0)); + assertFalse(isApproved); + } } From 5a71ed7749996f14d55a59a57816cc5394f299c9 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 9 Oct 2024 18:21:33 +0200 Subject: [PATCH 5/7] chore(NFTMetadataGeneratorSVG): add tests --- .../NFTMetadataGeneratorSVG.sol | 22 +++---- .../NFTMetadataGeneratorSVG.t.sol | 61 +++++++++++++++++++ 2 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol diff --git a/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol index f5c9496..0e1412d 100644 --- a/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol +++ b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol @@ -7,23 +7,23 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { BaseNFTMetadataGenerator } from "./BaseNFTMetadataGenerator.sol"; contract NFTMetadataGeneratorSVG is BaseNFTMetadataGenerator { - string private _imagePrefix = ""; - string private _imageSuffix = ""; + string public imagePrefix = ""; + string public imageSuffix = ""; - constructor(address nft, string memory imagePrefix, string memory imageSuffix) BaseNFTMetadataGenerator(nft) { - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; + constructor(address nft, string memory _imagePrefix, string memory _imageSuffix) BaseNFTMetadataGenerator(nft) { + imagePrefix = _imagePrefix; + imageSuffix = _imageSuffix; } - function setImageStrings(string memory imagePrefix, string memory imageSuffix) external onlyOwner { - _imagePrefix = imagePrefix; - _imageSuffix = imageSuffix; + function setImageStrings(string memory _imagePrefix, string memory _imageSuffix) external onlyOwner { + imagePrefix = _imagePrefix; + imageSuffix = _imageSuffix; } function generateImageURI(address, uint256 balance) internal view override returns (string memory) { - string memory text = Strings.toString(balance / 10e18); - bytes memory svg = abi.encodePacked(_imagePrefix, text, _imageSuffix); + string memory text = Strings.toString(balance / 1e18); + bytes memory svg = abi.encodePacked(imagePrefix, text, imageSuffix); - return Base64.encode(svg); + return string(abi.encodePacked("data:image/svg+xml;base64,", Base64.encode(svg))); } } diff --git a/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol b/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol new file mode 100644 index 0000000..c9709a0 --- /dev/null +++ b/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Test, console } from "forge-std/Test.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { MockToken } from "../mocks/MockToken.sol"; +import { NFTMetadataGeneratorSVG } from "../../src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol"; + +contract NFTMetadataGeneratorSVGTest is Test { + MockToken erc20Token; + NFTMetadataGeneratorSVG metadataGenerator; + + address alice = makeAddr("alice"); + + function setUp() public { + erc20Token = new MockToken("Test", "TEST"); + metadataGenerator = new NFTMetadataGeneratorSVG(address(erc20Token), "", ""); + + erc20Token.mint(alice, 10e18); + } + + function testGenerateMetadata() public view { + string memory expectedName = "XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6"; + string memory expectedDescription = + "This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000"; + string memory encodedImage = Base64.encode(abi.encodePacked("10")); + string memory expectedImage = string(abi.encodePacked("data:image/svg+xml;base64,", encodedImage)); + + bytes memory expectedMetadata = abi.encodePacked( + "{\"name\":\"", + expectedName, + "\",", + "\"description\":\"", + expectedDescription, + "\",", + "\"image\":\"", + expectedImage, + "\"}" + ); + + string memory metadata = metadataGenerator.generate(alice, 10e18); + assertEq(metadata, string(abi.encodePacked("data:application/json;base64,", Base64.encode(expectedMetadata)))); + } + + function testSetImageStrings() public { + assertEq(metadataGenerator.imagePrefix(), ""); + assertEq(metadataGenerator.imageSuffix(), ""); + + metadataGenerator.setImageStrings("", ""); + + assertEq(metadataGenerator.imagePrefix(), ""); + assertEq(metadataGenerator.imageSuffix(), ""); + } + + function testSetImageStringsRevert() public { + vm.prank(alice); + vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); + metadataGenerator.setImageStrings("", ""); + } +} From f36d85a358bf2d843dda477fddee832b6119ec59 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 9 Oct 2024 18:36:52 +0200 Subject: [PATCH 6/7] chore(XPNFTToken): add NFTMetadataGeneratorURL --- .../NFTMetadataGeneratorURL.sol | 24 +++++++++ .../NFTMetadataGeneratorURL.t.sol | 51 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/nft-metadata-generators/NFTMetadataGeneratorURL.sol create mode 100644 test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol diff --git a/src/nft-metadata-generators/NFTMetadataGeneratorURL.sol b/src/nft-metadata-generators/NFTMetadataGeneratorURL.sol new file mode 100644 index 0000000..e84d8d2 --- /dev/null +++ b/src/nft-metadata-generators/NFTMetadataGeneratorURL.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { BaseNFTMetadataGenerator } from "./BaseNFTMetadataGenerator.sol"; + +contract NFTMetadataGeneratorURL is BaseNFTMetadataGenerator { + string public urlPrefix; + string public urlSuffix; + + constructor(address nft, string memory _urlPrefix, string memory _urlSuffix) BaseNFTMetadataGenerator(nft) { + urlPrefix = _urlPrefix; + urlSuffix = _urlSuffix; + } + + function setURLStrings(string memory _urlPrefix, string memory _urlSuffix) external onlyOwner { + urlPrefix = _urlPrefix; + urlSuffix = _urlSuffix; + } + + function generateImageURI(address account, uint256) internal view override returns (string memory) { + return string(abi.encodePacked(urlPrefix, Strings.toHexString(account), urlSuffix)); + } +} diff --git a/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol b/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol new file mode 100644 index 0000000..0897af7 --- /dev/null +++ b/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import { Test, console } from "forge-std/Test.sol"; +import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { MockToken } from "../mocks/MockToken.sol"; +import { NFTMetadataGeneratorURL } from "../../src/nft-metadata-generators/NFTMetadataGeneratorURL.sol"; + +contract NFTMetadataGeneratorURLTest is Test { + MockToken erc20Token; + NFTMetadataGeneratorURL metadataGenerator; + + address alice = makeAddr("alice"); + + function setUp() public { + erc20Token = new MockToken("Test", "TEST"); + metadataGenerator = new NFTMetadataGeneratorURL(address(erc20Token), "http://test.local/images/", ".jpg"); + + erc20Token.mint(alice, 10e18); + } + + function testGenerateMetadata() public view { + string memory expectedMetadata = "data:application/json;base64,"; + bytes memory json = abi.encodePacked( + "{\"name\":\"XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6\",", + "\"description\":\"This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000\",", + "\"image\":\"http://test.local/images/0x328809bc894f92807417d2dad6b7c998c1afdac6.jpg\"}" + ); + expectedMetadata = string(abi.encodePacked(expectedMetadata, Base64.encode(json))); + + string memory metadata = metadataGenerator.generate(alice, erc20Token.balanceOf(alice)); + assertEq(metadata, expectedMetadata); + } + + function testSetBaseURL() public { + string memory newURLPrefix = "http://new-test.local/images/"; + string memory newURLSuffix = ".png"; + + metadataGenerator.setURLStrings(newURLPrefix, newURLSuffix); + + assertEq(metadataGenerator.urlPrefix(), newURLPrefix); + assertEq(metadataGenerator.urlSuffix(), newURLSuffix); + } + + function testSetBaseURLRevert() public { + vm.prank(alice); + vm.expectPartialRevert(Ownable.OwnableUnauthorizedAccount.selector); + metadataGenerator.setURLStrings("http://new-test.local/images/", ".png"); + } +} From ceed24573a54d82bcaf4c874492ddb8bd1410042 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Wed, 9 Oct 2024 18:44:32 +0200 Subject: [PATCH 7/7] chore: remove linter warning and errors --- src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol | 1 - .../NFTMetadataGeneratorSVG.t.sol | 9 +++++---- .../NFTMetadataGeneratorURL.t.sol | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol index 0e1412d..3dd659f 100644 --- a/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol +++ b/src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.26; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { BaseNFTMetadataGenerator } from "./BaseNFTMetadataGenerator.sol"; contract NFTMetadataGeneratorSVG is BaseNFTMetadataGenerator { diff --git a/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol b/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol index c9709a0..e725b1e 100644 --- a/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol +++ b/test/nft-metadata-generators/NFTMetadataGeneratorSVG.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { MockToken } from "../mocks/MockToken.sol"; import { NFTMetadataGeneratorSVG } from "../../src/nft-metadata-generators/NFTMetadataGeneratorSVG.sol"; contract NFTMetadataGeneratorSVGTest is Test { - MockToken erc20Token; - NFTMetadataGeneratorSVG metadataGenerator; + MockToken private erc20Token; + NFTMetadataGeneratorSVG private metadataGenerator; - address alice = makeAddr("alice"); + address private alice = makeAddr("alice"); function setUp() public { erc20Token = new MockToken("Test", "TEST"); @@ -23,6 +23,7 @@ contract NFTMetadataGeneratorSVGTest is Test { function testGenerateMetadata() public view { string memory expectedName = "XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6"; string memory expectedDescription = + // solhint-disable-next-line "This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000"; string memory encodedImage = Base64.encode(abi.encodePacked("10")); string memory expectedImage = string(abi.encodePacked("data:image/svg+xml;base64,", encodedImage)); diff --git a/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol b/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol index 0897af7..70118d3 100644 --- a/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol +++ b/test/nft-metadata-generators/NFTMetadataGeneratorURL.t.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.26; -import { Test, console } from "forge-std/Test.sol"; +import { Test } from "forge-std/Test.sol"; import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { MockToken } from "../mocks/MockToken.sol"; import { NFTMetadataGeneratorURL } from "../../src/nft-metadata-generators/NFTMetadataGeneratorURL.sol"; contract NFTMetadataGeneratorURLTest is Test { - MockToken erc20Token; - NFTMetadataGeneratorURL metadataGenerator; + MockToken private erc20Token; + NFTMetadataGeneratorURL private metadataGenerator; - address alice = makeAddr("alice"); + address private alice = makeAddr("alice"); function setUp() public { erc20Token = new MockToken("Test", "TEST"); @@ -24,6 +24,7 @@ contract NFTMetadataGeneratorURLTest is Test { string memory expectedMetadata = "data:application/json;base64,"; bytes memory json = abi.encodePacked( "{\"name\":\"XPNFT Token 0x328809bc894f92807417d2dad6b7c998c1afdac6\",", + // solhint-disable-next-line "\"description\":\"This is a XPNFT token for address 0x328809bc894f92807417d2dad6b7c998c1afdac6 with balance 10000000000000000000\",", "\"image\":\"http://test.local/images/0x328809bc894f92807417d2dad6b7c998c1afdac6.jpg\"}" );