Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport of custom uri suppport #62

Closed
wants to merge 5 commits into from
Closed
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
30 changes: 23 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@nestjs/websockets": "^8.0.11",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"lru-cache": "^7.10.1",
"nanoid": "^3.1.31",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.4.0",
Expand All @@ -44,6 +45,7 @@
"@nestjs/testing": "^8.0.11",
"@types/express": "^4.17.8",
"@types/jest": "^26.0.15",
"@types/lru-cache": "^7.10.10",
"@types/node": "^14.14.6",
"@types/supertest": "^2.0.10",
"@types/uuid": "^8.3.1",
Expand Down
56 changes: 51 additions & 5 deletions samples/solidity/contracts/ERC721WithData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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;
}
}
9 changes: 8 additions & 1 deletion samples/solidity/contracts/IERC721WithData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions samples/solidity/contracts/ITokenFactory.sol
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions samples/solidity/contracts/InterfaceCheck.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
18 changes: 13 additions & 5 deletions samples/solidity/contracts/TokenFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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;
}
}
8 changes: 5 additions & 3 deletions samples/solidity/test/ERC721WithData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,35 @@ describe('ERC721WithData - Unit Tests', function () {
deployedERC721WithData = await Factory.connect(deployerSignerA).deploy(
contractName,
contractSymbol,
""
);
await deployedERC721WithData.deployed();
});

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 () {
expect(await deployedERC721WithData.name()).to.equal(contractName);
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 () {
Expand Down
27 changes: 22 additions & 5 deletions samples/solidity/test/TokenFactory.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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');
Expand All @@ -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');
Expand Down
Loading