diff --git a/test/mocks/ERC721TokenReceiverMock.sol b/contracts/mocks/ERC721TokenReceiverMock.sol similarity index 100% rename from test/mocks/ERC721TokenReceiverMock.sol rename to contracts/mocks/ERC721TokenReceiverMock.sol diff --git a/test/mocks/XcertMock.sol b/contracts/mocks/XcertMock.sol similarity index 63% rename from test/mocks/XcertMock.sol rename to contracts/mocks/XcertMock.sol index b03974d..b722e58 100644 --- a/test/mocks/XcertMock.sol +++ b/contracts/mocks/XcertMock.sol @@ -2,12 +2,14 @@ pragma solidity ^0.4.19; import "../../contracts/tokens/BurnableXcert.sol"; import "../../contracts/tokens/SettableTransferXcert.sol"; +import "../../contracts/tokens/ChainableXcert.sol"; -contract XcertMock is BurnableXcert, SettableTransferXcert { +contract XcertMock is BurnableXcert, SettableTransferXcert, ChainableXcert { function XcertMock(string _name, string _symbol) BurnableXcert(_name, _symbol) SettableTransferXcert(_name, _symbol) + ChainableXcert(_name, _symbol) public { diff --git a/contracts/tokens/ChainableXcert.sol b/contracts/tokens/ChainableXcert.sol new file mode 100644 index 0000000..ebb4592 --- /dev/null +++ b/contracts/tokens/ChainableXcert.sol @@ -0,0 +1,68 @@ +pragma solidity ^0.4.19; + +import "./Xcert.sol"; + +contract ChainableXcert is Xcert { + + /* + * @dev This emits when additional proof is chained. + */ + event ChainedProof(uint256 indexed tokenId, string proof, uint256 index); + + function ChainableXcert(string _name, + string _symbol) + Xcert(_name, _symbol) + public + { + supportedInterfaces[0xc1dcb551] = true; // ChainableXcert + } + + /* + * @dev Adds new proof to chain of proofs for token. + * @param _tokenId Id of Xcert that we want to add proof to. + * @param _proof New proof we want to add to NFToken. + */ + function chain(uint256 _tokenId, + string _proof) + validNFToken(_tokenId) + onlyOwner + external + { + + require(bytes(_proof).length > 0); + + idToProof[_tokenId].push(_proof); + + ChainedProof(_tokenId, _proof, idToProof[_tokenId].length.sub(1)); + } + + /* + * @dev Gets proof by index. + * @param _tokenId Id of the Xcert we want to get proof of. + * @param _index Index of the proof we want to get. + */ + function getProofByIndex(uint256 _tokenId, + uint256 _index) + validNFToken(_tokenId) + external + view + returns (string) + { + require(_index < idToProof[_tokenId].length); + return idToProof[_tokenId][_index]; + } + + /* + * @dev Gets the count of all proofs for a Xcert. + * @param _tokenId Id of the Xcert we want to get proof of. + */ + function getProofCount(uint256 _tokenId) + validNFToken(_tokenId) + external + view + returns (uint256) + { + return idToProof[_tokenId].length; + } + +} \ No newline at end of file diff --git a/contracts/tokens/Xcert.sol b/contracts/tokens/Xcert.sol index 0e4d704..178ae69 100644 --- a/contracts/tokens/Xcert.sol +++ b/contracts/tokens/Xcert.sol @@ -53,7 +53,7 @@ contract Xcert is Ownable, ERC721, ERC721Metadata, ERC165 { /* * @dev Mapping from NFToken ID to proof. */ - mapping (uint256 => string) internal idToProof; + mapping (uint256 => string[]) internal idToProof; /* * @dev Mapping of supported intefraces. @@ -103,11 +103,6 @@ contract Xcert is Ownable, ERC721, ERC721Metadata, ERC165 { */ event MintAuthorizedAddress(address indexed _target, bool _authorized); - /* - * @dev this emits everytime a new Xcert contract is deployed. - */ - event XcertContractDeployed(address _contractAddress, string _name, string _symbol); - /* * @dev Guarantees that the msg.sender is an owner or operator of the given NFToken. * @param _tokenId ID of the NFToken to validate. @@ -164,7 +159,6 @@ contract Xcert is Ownable, ERC721, ERC721Metadata, ERC165 { supportedInterfaces[0x80ac58cd] = true; // ERC721 supportedInterfaces[0x5b5e139f] = true; // ERC721Metadata supportedInterfaces[0x58c66b9f] = true; // Xcert - XcertContractDeployed(address(this), _name, _symbol); } /* @@ -391,7 +385,7 @@ contract Xcert is Ownable, ERC721, ERC721Metadata, ERC165 { require(bytes(_proof).length > 0); idToUri[_id] = _uri; - idToProof[_id] = _proof; + idToProof[_id].push(_proof); addNFToken(_to, _id); Transfer(address(0), _to, _id); @@ -408,7 +402,7 @@ contract Xcert is Ownable, ERC721, ERC721Metadata, ERC165 { view returns (string) { - return idToProof[_tokenId]; + return idToProof[_tokenId][idToProof[_tokenId].length.sub(1)]; } /* diff --git a/contracts/utils/Selector.sol b/contracts/utils/Selector.sol index 20c8b6f..1f58f05 100644 --- a/contracts/utils/Selector.sol +++ b/contracts/utils/Selector.sol @@ -3,6 +3,7 @@ pragma solidity ^0.4.19; import "../tokens/Xcert.sol"; import "../tokens/BurnableXcert.sol"; import "../tokens/SettableTransferXcert.sol"; +import "../tokens/ChainableXcert.sol"; contract Selector { @@ -23,4 +24,11 @@ contract Selector { SettableTransferXcert i; return i.setTransferable.selector; } + + function calculateChainableXcertSelector() public pure returns (bytes4) { + ChainableXcert i; + return i.chain.selector + ^ i.getProofByIndex.selector + ^ i.getProofCount.selector; + } } \ No newline at end of file diff --git a/test/tokens/ChainableXcert.test.js b/test/tokens/ChainableXcert.test.js new file mode 100644 index 0000000..83b0233 --- /dev/null +++ b/test/tokens/ChainableXcert.test.js @@ -0,0 +1,44 @@ +const ChainableXcert = artifacts.require('ChainableXcert'); +const util = require('ethjs-util'); +const assertRevert = require('../helpers/assertRevert'); + +contract('ChainableXcert', (accounts) => { + let xcert; + let id1 = web3.sha3('test1'); + let mockProof = "1e205550c271490347e5e2393a02e94d284bbe9903f023ba098355b8d75974c8"; + let mockProof2 = "1e205550c271490347e5e2393a02e94d284bbe9903f023ba098355b8d75974d5"; + + beforeEach(async function () { + xcert = await ChainableXcert.new('Foo', 'F'); + await xcert.mint(accounts[0], id1, mockProof, 'url1'); + }); + + it('correctly chains additional proof', async () => { + await xcert.chain(id1, mockProof2); + var proof = await xcert.getProof(id1); + assert.equal(proof, mockProof2); + }); + + it('reverts trying to chain empty proof', async () => { + await assertRevert(xcert.chain(id1, "")); + }); + + it('correctly gets proof by index', async () => { + await xcert.chain(id1, mockProof2); + var proof = await xcert.getProofByIndex(id1, 1); + assert.equal(proof, mockProof2); + }); + + it('reverts trying to get proof for none existant index', async () => { + await assertRevert(xcert.getProofByIndex(id1, 1)); + }); + + it('returns correct proof count', async () => { + var proofCount = await xcert.getProofCount(id1); + assert.equal(proofCount, 1); + + await xcert.chain(id1, mockProof2); + proofCount = await xcert.getProofCount(id1); + assert.equal(proofCount, 2); + }); +}); diff --git a/test/tokens/XcertMock.test.js b/test/tokens/XcertMock.test.js index 6b44403..500f6ea 100644 --- a/test/tokens/XcertMock.test.js +++ b/test/tokens/XcertMock.test.js @@ -9,22 +9,28 @@ contract('XcertMock', (accounts) => { let id3 = web3.sha3('test3'); let id4 = web3.sha3('test4'); let mockProof = "1e205550c271490347e5e2393a02e94d284bbe9903f023ba098355b8d75974c8"; + let mockProof2 = "1e205550c271490347e5e2393a02e94d284bbe9903f023ba098355b8d75974d7"; beforeEach(async function () { xcert = await Xcert.new('Foo', 'F'); + await xcert.mint(accounts[0], id1, mockProof, 'url1'); }); it('destroys NFToken id 1', async () => { - await xcert.mint(accounts[0], id1, mockProof, 'url1'); await xcert.burn(id1); const count = await xcert.balanceOf(accounts[0]); assert.equal(count, 0); }); it('reverts trying to transfer when transfers are disabled', async () => { - await xcert.mint(accounts[0], id1, mockProof, 'url1'); await xcert.setTransferable(false); await assertRevert(xcert.transferFrom(accounts[0], accounts[1], id1)); }); + it('correctly chains additional proof', async () => { + await xcert.chain(id1, mockProof2); + var proof = await xcert.getProof(id1); + assert.equal(proof, mockProof2); + }); + }); diff --git a/test/utils/Selector.test.js b/test/utils/Selector.test.js new file mode 100644 index 0000000..7b6f01b --- /dev/null +++ b/test/utils/Selector.test.js @@ -0,0 +1,43 @@ +const Xcert = artifacts.require('Xcert'); +const BurnableXcert = artifacts.require('BurnableXcert'); +const ChainableXcert = artifacts.require('ChainableXcert'); +const SettableTransferXcert = artifacts.require('SettableTransferXcert'); +const Selector = artifacts.require('Selector'); + +contract('Selector', (accounts) => { + + let selector; + + beforeEach(async function () { + selector = await Selector.new(); + }); + + it('Checks Xcert selector', async () => { + var xcert = await Xcert.new('Foo', 'F'); + var bytes = await selector.calculateXcertSelector(); + var supports = await xcert.supportsInterface(bytes); + assert.equal(supports, true); + }); + + it('Checks BurnableXcert selector', async () => { + var xcert = await BurnableXcert.new('Foo', 'F'); + var bytes = await selector.calculateBurnableXcertSelector(); + var supports = await xcert.supportsInterface(bytes); + assert.equal(supports, true); + }); + + it('Checks SettableTransferXcert selector', async () => { + var xcert = await SettableTransferXcert.new('Foo', 'F'); + var bytes = await selector.calculateSettableTransferXcertSelector(); + var supports = await xcert.supportsInterface(bytes); + assert.equal(supports, true); + }); + + it('Checks ChainableXcert selector', async () => { + var xcert = await ChainableXcert.new('Foo', 'F'); + var bytes = await selector.calculateChainableXcertSelector(); + var supports = await xcert.supportsInterface(bytes); + assert.equal(supports, true); + }); + +});