diff --git a/samples/solidity/contracts/ERC721WithData.sol b/samples/solidity/contracts/ERC721WithData.sol index fcd8c7e..dad4717 100644 --- a/samples/solidity/contracts/ERC721WithData.sol +++ b/samples/solidity/contracts/ERC721WithData.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.0; -import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; -import '@openzeppelin/contracts/utils/Context.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts/utils/Context.sol'; +import "@openzeppelin/contracts/utils/Strings.sol"; +import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import './IERC721WithData.sol'; /** @@ -14,7 +15,7 @@ import './IERC721WithData.sol'; * - the contract owner (ie deployer) is the only party allowed to mint * - any party can approve another party to manage (ie transfer) some or all of their tokens * - any party can burn their own tokens - * - token URIs are hard-coded to "firefly://token/{id}" + * - token URIs are customizable when minting, but default to "firefly://token/{id}" * * The inclusion of a "data" argument on each external method allows FireFly to write * extra data to the chain alongside each token transaction, in order to correlate it with @@ -23,7 +24,17 @@ import './IERC721WithData.sol'; * This is a sample only and NOT a reference implementation. */ contract ERC721WithData is Context, Ownable, ERC721, IERC721WithData { - constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + string private _baseTokenURI; + // Optional mapping for token URIs + mapping (uint256 => string) private _tokenURIs; + + constructor( + string memory name, + string memory symbol, + string memory baseTokenURI + ) ERC721(name, symbol) { + _baseTokenURI = baseTokenURI; + } function supportsInterface( bytes4 interfaceId @@ -39,6 +50,24 @@ contract ERC721WithData is Context, Ownable, ERC721, IERC721WithData { bytes calldata data ) external override onlyOwner { _safeMint(to, tokenId, data); + _setTokenURI(tokenId, string(abi.encodePacked(_baseURI(), Strings.toString(tokenId)))); + } + + function mintWithURI( + address to, + uint256 tokenId, + bytes calldata data, + string memory tokenURI_ + ) external override onlyOwner { + _safeMint(to, tokenId, data); + + // If there is no tokenURI passed, concatenate the tokenID to the base URI + bytes memory tempURITest = bytes(tokenURI_); + if (tempURITest.length == 0) { + _setTokenURI(tokenId, string(abi.encodePacked(_baseURI(), Strings.toString(tokenId)))); + } else { + _setTokenURI(tokenId, tokenURI_); + } } function transferWithData( @@ -76,6 +105,23 @@ contract ERC721WithData is Context, Ownable, ERC721, IERC721WithData { } function _baseURI() internal view virtual override returns (string memory) { - return 'firefly://token/'; + bytes memory tempURITest = bytes(_baseTokenURI); + if (tempURITest.length == 0) { + return 'firefly://token/'; + } else { + return _baseTokenURI; + } + } + + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_exists(tokenId), "ERC721WithData: Token does not exist"); + + string memory uri = _tokenURIs[tokenId]; + return uri; + } + + function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal onlyOwner { + require(_exists(tokenId), "ERC721WithData: Token does not exist"); + _tokenURIs[tokenId] = _tokenURI; } } diff --git a/samples/solidity/contracts/IERC721WithData.sol b/samples/solidity/contracts/IERC721WithData.sol index 281faca..4193585 100644 --- a/samples/solidity/contracts/IERC721WithData.sol +++ b/samples/solidity/contracts/IERC721WithData.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/introspection/IERC165.sol'; /** - * ERC721 interface with mint, burn, and attached data support. + * ERC721 interface with mint, burn, attached data, and custom URI support. * * The inclusion of a "data" argument on each external method allows FireFly to write * extra data to the chain alongside each token transaction, in order to correlate it with @@ -18,6 +18,13 @@ interface IERC721WithData is IERC165 { bytes calldata data ) external; + function mintWithURI( + address to, + uint256 tokenId, + bytes calldata data, + string memory tokenURI_ + ) external; + function transferWithData( address from, address to, diff --git a/samples/solidity/contracts/ITokenFactory.sol b/samples/solidity/contracts/ITokenFactory.sol new file mode 100644 index 0000000..6d8c548 --- /dev/null +++ b/samples/solidity/contracts/ITokenFactory.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/utils/introspection/IERC165.sol'; + +/** + * TokenFactory interface with data and custom URI support. + */ + +interface ITokenFactory is IERC165 { + function create( + string memory name, + string memory symbol, + bool is_fungible, + bytes calldata data, + string memory uri + ) external; +} diff --git a/samples/solidity/contracts/InterfaceCheck.sol b/samples/solidity/contracts/InterfaceCheck.sol index 7fd9d11..88ccdba 100644 --- a/samples/solidity/contracts/InterfaceCheck.sol +++ b/samples/solidity/contracts/InterfaceCheck.sol @@ -4,11 +4,16 @@ pragma solidity ^0.8.0; import './IERC20WithData.sol'; import './IERC721WithData.sol'; +import './ITokenFactory.sol'; /** * Test utility for checking ERC165 interface identifiers. */ contract InterfaceCheck { + function tokenfactory() external view returns (bytes4) { + return type(ITokenFactory).interfaceId; + } + function erc20WithData() external view returns (bytes4) { return type(IERC20WithData).interfaceId; } diff --git a/samples/solidity/contracts/TokenFactory.sol b/samples/solidity/contracts/TokenFactory.sol index 53497e6..be5173f 100644 --- a/samples/solidity/contracts/TokenFactory.sol +++ b/samples/solidity/contracts/TokenFactory.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/Context.sol'; import './ERC20WithData.sol'; import './ERC721WithData.sol'; +import './ITokenFactory.sol'; /** * Example TokenFactory for deploying simple ERC20 and ERC721 token contracts. @@ -22,7 +23,7 @@ import './ERC721WithData.sol'; * Just a few of the questions to consider when developing a contract for production: * - is a factory pattern the best solution for your use case, or is a pre-deployed token contract more suitable? * - is a proxy layer needed for contract upgradeability? - * - are other extension points beyond "name" and "symbol" needed (for instance "decimals", "uri", "supply")? + * - are other extension points beyond "name", "symbol", and "uri" needed (for instance "decimals" or "supply")? * * See the FireFly documentation for descriptions of the various patterns supported for working with tokens. * Please also read the descriptions of the sample ERC20WithData and ERC721WithData contracts utilized by this @@ -31,23 +32,30 @@ import './ERC721WithData.sol'; * Finally, remember to always consult best practices from other communities and examples (such as OpenZeppelin) * when crafting your token logic, rather than relying on the FireFly community alone. Happy minting! */ -contract TokenFactory is Context { +contract TokenFactory is Context, ITokenFactory { event TokenPoolCreation(address indexed contract_address, string name, string symbol, bool is_fungible, bytes data); function create( string memory name, string memory symbol, bool is_fungible, - bytes calldata data - ) external virtual { + bytes calldata data, + string memory uri + ) external override virtual { if (is_fungible) { ERC20WithData erc20 = new ERC20WithData(name, symbol); erc20.transferOwnership(_msgSender()); emit TokenPoolCreation(address(erc20), name, symbol, true, data); } else { - ERC721WithData erc721 = new ERC721WithData(name, symbol); + ERC721WithData erc721 = new ERC721WithData(name, symbol, uri); erc721.transferOwnership(_msgSender()); emit TokenPoolCreation(address(erc721), name, symbol, false, data); } } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165) returns (bool) { + return interfaceId == type(ITokenFactory).interfaceId; + } } diff --git a/samples/solidity/test/ERC721WithData.ts b/samples/solidity/test/ERC721WithData.ts index 2d8289f..e5a6b25 100644 --- a/samples/solidity/test/ERC721WithData.ts +++ b/samples/solidity/test/ERC721WithData.ts @@ -22,6 +22,7 @@ describe('ERC721WithData - Unit Tests', function () { deployedERC721WithData = await Factory.connect(deployerSignerA).deploy( contractName, contractSymbol, + "" ); await deployedERC721WithData.deployed(); }); @@ -29,7 +30,7 @@ describe('ERC721WithData - Unit Tests', function () { it('Verify interface ID', async function () { const checkerFactory = await ethers.getContractFactory('InterfaceCheck'); const checker: InterfaceCheck = await checkerFactory.connect(deployerSignerA).deploy(); - expect(await checker.erc721WithData()).to.equal('0xb2429c12'); + expect(await checker.erc721WithData()).to.equal('0xfd0771df'); }); it('Create - Should create a new ERC721 instance with default state', async function () { @@ -37,18 +38,19 @@ describe('ERC721WithData - Unit Tests', function () { expect(await deployedERC721WithData.symbol()).to.equal(contractSymbol); }); - it('Mint - Deployer should mint tokens to itself successfully', async function () { + it('Mint - Should mint successfully with a custom URI', async function () { expect(await deployedERC721WithData.balanceOf(deployerSignerA.address)).to.equal(0); // Signer A mint token 721 to Signer A (Allowed) await expect( deployedERC721WithData .connect(deployerSignerA) - .mintWithData(deployerSignerA.address, 721, '0x00'), + .mintWithURI(deployerSignerA.address, 721, '0x00', "ipfs://CID"), ) .to.emit(deployedERC721WithData, 'Transfer') .withArgs(ZERO_ADDRESS, deployerSignerA.address, 721); expect(await deployedERC721WithData.balanceOf(deployerSignerA.address)).to.equal(1); + expect(await deployedERC721WithData.tokenURI(721)).to.equal('ipfs://CID'); }); it('Mint - Non-deployer of contract should not be able to mint tokens', async function () { diff --git a/samples/solidity/test/TokenFactory.ts b/samples/solidity/test/TokenFactory.ts index fcfe152..76a1af4 100644 --- a/samples/solidity/test/TokenFactory.ts +++ b/samples/solidity/test/TokenFactory.ts @@ -1,13 +1,11 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { TokenFactory } from '../typechain'; +import { TokenFactory, InterfaceCheck } from '../typechain'; describe('TokenFactory - Unit Tests', function () { const contractName = 'testName'; const contractSymbol = 'testSymbol'; - const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; - const ONE_ADDRESS = '0x1111111111111111111111111111111111111111'; let deployedTokenFactory: TokenFactory; let Factory; @@ -23,8 +21,14 @@ describe('TokenFactory - Unit Tests', function () { await deployedTokenFactory.deployed(); }); + it('Verify interface ID', async function () { + const checkerFactory = await ethers.getContractFactory('InterfaceCheck'); + const checker: InterfaceCheck = await checkerFactory.connect(deployerSignerA).deploy(); + expect(await checker.tokenfactory()).to.equal('0x83a74a0c'); + }); + it('Create - Should deploy a new ERC20 contract', async function () { - const tx = await deployedTokenFactory.create(contractName, contractSymbol, true, '0x00'); + const tx = await deployedTokenFactory.create(contractName, contractSymbol, true, '0x00', ''); expect(tx).to.emit(deployedTokenFactory, 'TokenPoolCreation'); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === 'TokenPoolCreation'); @@ -37,7 +41,20 @@ describe('TokenFactory - Unit Tests', function () { }); it('Create - Should deploy a new ERC721 contract', async function () { - const tx = await deployedTokenFactory.create(contractName, contractSymbol, false, '0x00'); + const tx = await deployedTokenFactory.create(contractName, contractSymbol, false, '0x00', ''); + expect(tx).to.emit(deployedTokenFactory, 'TokenPoolCreation'); + const receipt = await tx.wait(); + const event = receipt.events?.find(e => e.event === 'TokenPoolCreation'); + expect(event).to.exist; + if (event) { + expect(event.args).to.have.length(5); + expect(event.args?.[0]).to.be.properAddress; + expect(event.args?.slice(1)).to.eql([contractName, contractSymbol, false, '0x00']); + } + }); + + it('Create - Should deploy a new ERC721 contract with a custom URI', async function () { + const tx = await deployedTokenFactory.create(contractName, contractSymbol, false, '0x00', 'testURI'); expect(tx).to.emit(deployedTokenFactory, 'TokenPoolCreation'); const receipt = await tx.wait(); const event = receipt.events?.find(e => e.event === 'TokenPoolCreation');