New tool to add to our toolkit : Truffle boxes !
“Truffle Boxes are helpful boilerplates that allow you to focus on what makes your DApp unique. In addition to Truffle, Truffle Boxes can contain other helpful modules, Solidity contracts & libraries, front-end views and more; all the way up to complete example dapps”
It basically provides with a server for the front end and boilerplates to ease the development process of your dapp.
Setup the project :
- Verify you have Truffle installed (In your terminal use
truffle -v
) if not you need to install it (in your terminal usenpm install -g truffle
). - Create your project folder (In your terminal use
mkdir starNotaryv1
) - Move to the project folder (
cd starNotaryv1
) - Create your project using (In your terminal
truffle unbox webpack
)
Then let’s start the truffle environment and connect to the local RPC on metamask :
- In your project folder (in your terminal use the command
truffle develop
orsudo truffle develop
). - Check Truffle started in the URl:
http://127.0.0.1:9545/
And then in metamask simply connect to the local ethereum network :
You can now also add the private keys (import an account) from your truffle dev environment to your metamask account, simply click on the account image top righ, chose “import account” and copy paste one of the private keys from your truffle dev env.
Alright let’s deploy on remix our first version of the star notary smart contract now :
pragma solidity >=0.4.24; contract StarNotary { string public starName; address public starOwner; event starClaimed(address owner); constructor() public { starName = "Awesome Udacity Star"; // we initiate the value of the star name } function claimStar() public { // we can claim the star starOwner = msg.sender; emit starClaimed(msg.sender); } }
Tests with Mocha and Chai
Mocha and Cha
- Mocha is a Testing Framework for Javascript, it can be used for front end applications and back end applications like Ethereum Decentralized Apps.
- Chai is an Assertion Library
- Both are available as NPM packages
- Very popular between developer for testing their code
- Truffle supports and comes preinstalled with Mocha and Chai
beforeEach (async () => { library = await Library.deployed(); await contract ('Udacity', function(acc) { accounts = acc; }) }); describe ('all tests', () => { it ('can create a Library owner', async () => { const owner = await library.owner(); assert.equal(owner, accounts[0]); }) })
this is just some example code. Tests are grouped underneath the describe keyword, and test cases are grouped under the it keyword.
Alright, setting up the env round 2 :
We deleted the contracts besides Migrations.sol and then created a new smart contract StarNotary.sol, we then modified 2_deploy_contracts.js to remove the previous contracts and add our new StarNotary.sol contract :
And finally we put the code into the TestStarNotary.js :
// Importing the StartNotary Smart Contract ABI (JSON representation of the Smart Contract) const StarNotary = artifacts.require("StarNotary"); var accounts; // List of development accounts provided by Truffle var owner; // Global variable use it in the tests cases // This called the StartNotary Smart contract and initialize it -> test is done in clean contract contract('StarNotary', (accs) => { accounts = accs; // Assigning test accounts owner = accounts[0]; // Assigning the owner test account }); // Example test case, it will test if the contract is able to return the starName property // initialized in the contract constructor it('has correct name', async () => { let instance = await StarNotary.deployed(); // Making sure the Smart Contract is deployed and getting the instance. let starName = await instance.starName.call(); // Calling the starName property assert.equal(starName, "Awesome Udacity Star"); // Assert if the starName property was initialized correctly });
It is important to remember that all this test libraries use Promises, that’s why we are using in each function the keywords async
and await
. Also interesting to notice that the .call() does not require any gas as it calls the contract to get data without changing the state.
And finally whern you have your test .js file in your test folder then simply run “test” in truffle(develop) :
We now add a new test case in the script to test if the star is indeed claimed by the owner
// Example test case, it will test is the Smart Contract function claimStar assigned the Star to the owner address it('can be claimed', async () => { let instance = await StarNotary.deployed(); // Making sure the Smart Contract is deployed and getting the instance. await instance.claimStar({from: owner}); // Calling the Smart Contract function claimStar let starOwner = await instance.starOwner.call(); // Getting the owner address assert.equal(starOwner, owner); // Verifying if the owner address match with owner of the address });
Finally we add a new test case in the script to test the change of the owner of the star :
// Example test case, it will test is the Smart Contract function claimStar assigned the Star to the owner address and it can be changed it('can change owners', async () => { let instance = await StarNotary.deployed(); let secondUser = accounts[1]; //this will be the second owner of the star await instance.claimStar({from: owner}); //we claim the instance with the by default owner let starOwner = await instance.starOwner.call(); // we retrieve the value assert.equal(starOwner, owner); //we check await instance.claimStar({from: secondUser}); //we claim with the second owner let secondOwner = await instance.starOwner.call(); // we retrieve the value assert.equal(secondOwner, secondUser); //we check });
And finally let’s add a function to change the name of the star and a piece of code to test it :
//Changing the name, to be added into the StarNotary.sol function changeName (string memory _name) public { starName = _name; } //and the testing to be added in the TestStarNotary.js it('can change names', async () => { await instance.claimStar({from: owner}); await instance.changeName('New Name', {from: owner}); assert.equal(await instance.starName.call(), 'New Name'); })
Had to also add the following into the test file in order to make sure to refresh the instance of the contract with the latest deployed one, other wise I had an error “ReferenceError: instance is not defined” regarding the new function that was added (changeName).
beforeEach(async () => { instance = await StarNotary.deployed(); });
All tests are passing, we are good !
Creating the Frond end
The cool thing about the truffle webpack box that we installed is that it comes already with boilerplates for the frontend in the form of html, css and .js, right here :
let’s replace the code for index.html with our first version of the front end :
<!DOCTYPE html> <html> <head> <title>StarNotary DAPP Front End</title> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'> </head> <style> input { display: block; margin-bottom: 12px; } </style> <body> <h1>StarNotary DAPP</h1> <br><label for="name">Star Name:</label><h3 id='name'>Star Name: </h3> <br><br><button onclick="App.starNameFunc()">Get Star Name</button> <hr> <br><label for="owner">Star Owner:</label><h3 id='owner'>Star Owner: </h3> <br><br><button onclick="App.starOwnerFunc()">Get Star Owner</button> <br><br> <hr> <br> <h1>Claim Star</h1> <br><br><button id="claimStar" onclick="App.claimStarFunc()">Claim a Star</button> <br><br> <br> <hr> <span id="status"></span> <br> <script src="index.js"></script> </body> </html>
Now we have all the keys to run our complete dapp, with the smart contract and the front end.
Here how to run it :
Before running your application you need to do the last step and it is to change in the truffle-config.js
file the network configuration to make sure Metamask is able to connect with your development environment:
networks: { // Useful for testing. The `development` name is special - truffle uses it by default // if it's defined here and no other network is specified at the command line. // You should run a client (like ganache-cli, geth or parity) in a separate terminal // tab if you use this network and you must also set the `host`, `port` and `network_id` // options below to some value. // development: { host: "127.0.0.1", // Localhost (default: none) port: 8545, // Standard Ethereum port (default: none) network_id: "*", // Any network (default: none) }, ...
To run your application you will need to:
- Run
truffle develop
orsudo truffle develop
commands. - Run
compile
command. - Run
migrate --reset
Then
- Open a second terminal window, and make sure you are inside your project directory.
- Run
cd app
to move inside the app folder. - Run
npm run dev
command.
This will start our port at 8080. Open http://localhost:8080/ in your browser. Make sure the Metamask extension is installed, and you are logged into it, and also have imported the accounts.
It works ! As soon as you get onto it it opens up metamask and if you are on the custom local network you can interact with the smart contract, getting the name of the star, claiming it, getting the owner address. First Dapp done !!!
what we did :
- Smart contract coding in remix
- Truffle boxes for the boiler plate code
- Contract to truffle
- Test cases
- Designed the front end
- Started the server to host
- Metamask connected to private network (port 8545)
Alright now we will create the stars as NFT with ERC-721
ERC-721
If you want more information regarding this kind of token : https://brax.gg/nfts-new-frenzy-tokens/
The description of the token can aussi be found here : http://erc721.org/
Alright so we are going now to create a new folder for the new project and add a new library : openzeppelin-solidity
truffle unbox webpack
npm install openzeppelin-solidity
We create a new smart contract in the contract folder called StarNotary.sol
It was a pain to adapt the contract because the code of the training is not updated but here wo go :
pragma solidity >=0.5.0; //in StarNotary.sol import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol"; contract StarNotary is ERC721 { constructor() public ERC721("GameItem", "ITM"){} struct Star { string name; } mapping(uint256 => Star) public tokenIdToStarInfo; mapping(uint256 => uint256) public starsForSale; // Create Star using the Struct function createStar(string memory _name, uint256 _tokenId) public { // Passing the name and tokenId as a parameters Star memory newStar = Star(_name); // Star is an struct so we are creating a new Star tokenIdToStarInfo[_tokenId] = newStar; // Creating in memory the Star -> tokenId mapping _mint(msg.sender, _tokenId); // _mint assign the the star with _tokenId to the sender address (ownership) } // Putting an Star for sale (Adding the star tokenid into the mapping starsForSale, first verify that the sender is the owner) function putStarUpForSale(uint256 _tokenId, uint256 _price) public { require(ownerOf(_tokenId) == msg.sender, "You can't sale the Star you don't owned"); starsForSale[_tokenId] = _price; } // Function that allows you to convert an address into a payable address //function _make_payable(address x) internal pure returns (address payable) { // return payable(x); //} function buyStar(uint256 _tokenId) public payable { require(starsForSale[_tokenId] > 0, "The Star should be up for sale"); uint256 starCost = starsForSale[_tokenId]; address ownerAddress = ownerOf(_tokenId); require(msg.value > starCost, "You need to have enough Ether"); transferFrom(ownerAddress, msg.sender, _tokenId); // We can't use _addTokenTo or_removeTokenFrom functions, now we have to use _transferFrom //address payable ownerAddressPayable = _make_payable(ownerAddress); // We need to make this conversion to be able to use transfer() function to transfer ethers //address payable addr3 = address(uint160(ownerAddress)); address payable addr3 = payable(ownerAddress); addr3.transfer(starCost); if(msg.value > starCost) { address payable addr4 = payable(msg.sender); //address payable addr4 = address(uint160(msg.sender)); addr4.transfer(msg.value - starCost); } } }
//in 2_deploy_contract.js const StarNotary = artifacts.require("StarNotary"); module.exports = function(deployer) { deployer.deploy(StarNotary); };
I also had to change the version in the truffle-config.js to : version: “0.8.0”, and to change the limitation regarding the version in the Migrations.sol file.
Finally it compiled properly :
Now let’s run a few tests, here as well I had to bug fix to make it work with latest version of ther ERC-721 :
const StarNotary = artifacts.require("StarNotary"); var accounts; var owner; contract('StarNotary', (accs) => { accounts = accs; owner = accounts[0]; }); it('can Create a Star', async() => { let tokenId = 1; let instance = await StarNotary.deployed(); await instance.createStar('Awesome Star!', tokenId, {from: accounts[0]}) //creation of a star assert.equal(await instance.tokenIdToStarInfo.call(tokenId), 'Awesome Star!') //check name of star }); it('lets user1 put up their star for sale', async() => { let instance = await StarNotary.deployed(); let user1 = accounts[1]; let starId = 2; let starPrice = web3.utils.toWei(".01", "ether"); await instance.createStar('awesome star', starId, {from: user1}); //creation of star await instance.putStarUpForSale(starId, starPrice, {from: user1}); // put for sale assert.equal(await instance.starsForSale.call(starId), starPrice); //check that the star is for sale }); it("lets user1 get the funds after the sale", async () => { let instance = await StarNotary.deployed(); let user1 = accounts[1]; let user2 = accounts[2]; let starId = 3; let starPrice = web3.utils.toWei(".01", "ether"); let balance = web3.utils.toWei(".05", "ether"); await instance.createStar("awesome star", starId, { from: user1 }); //creation of star await instance.putStarUpForSale(starId, starPrice, { from: user1 }); // put for sale let balanceOfUser1BeforeTransaction = await web3.eth.getBalance(user1); //balance of user //requires that the caller is the owner or approved to transfer that token. `user2` //is not the token owner so it has to be approved. await instance.approve(user2, starId, {from: user1, gasPrice: 0}) await instance.buyStar(starId, { from: user2, value: balance, gasPrice: 0}); let balanceOfUser1AfterTransaction = await web3.eth.getBalance(user1); //Check that the funds were transmitted let value1 = Number(balanceOfUser1BeforeTransaction) + Number(starPrice); let value2 = Number(balanceOfUser1AfterTransaction); assert.equal(value1, value2); }); it('lets user2 buy a star, if it is put up for sale', async() => { let instance = await StarNotary.deployed(); let user1 = accounts[1]; let user2 = accounts[2]; let starId = 4; let starPrice = web3.utils.toWei(".01", "ether"); let balance = web3.utils.toWei(".05", "ether"); await instance.createStar('awesome star', starId, {from: user1}); await instance.putStarUpForSale(starId, starPrice, {from: user1}); let balanceOfUser1BeforeTransaction = await web3.eth.getBalance(user2); //requires that the caller is the owner or approved to transfer that token. `user2` //is not the token owner so it has to be approved. await instance.approve(user2, starId, {from: user1, gasPrice: 0}) await instance.buyStar(starId, {from: user2, value: balance}); //Check that the star was sold assert.equal(await instance.ownerOf.call(starId), user2); }); it('lets user2 buy a star and decreases its balance in ether', async() => { let instance = await StarNotary.deployed(); let user1 = accounts[1]; let user2 = accounts[2]; let starId = 5; let starPrice = web3.utils.toWei(".01", "ether"); let balance = web3.utils.toWei(".05", "ether"); await instance.createStar('awesome star', starId, {from: user1}); await instance.putStarUpForSale(starId, starPrice, {from: user1}); let balanceOfUser1BeforeTransaction = await web3.eth.getBalance(user2); const balanceOfUser2BeforeTransaction = await web3.eth.getBalance(user2); //requires that the caller is the owner or approved to transfer that token. `user2` //is not the token owner so it has to be approved. await instance.approve(user2, starId, {from: user1, gasPrice: 0}) await instance.buyStar(starId, {from: user2, value: balance, gasPrice:0}); const balanceAfterUser2BuysStar = await web3.eth.getBalance(user2); let value = Number(balanceOfUser2BeforeTransaction) - Number(balanceAfterUser2BuysStar); //Check that the funds were transmitted assert.equal(value, starPrice); });
Next we do the front end, as before by replacing the content of the html file in the app folder
<!DOCTYPE html> <html> <head> <title>StarNotary DApp</title> </head> <style> input { display: block; margin-bottom: 12px; } </style> <body> <h1>StarNotary Token DAPP</h1> <hr> <br> <h1>Create a Star</h1> <br><label for="starName">Star Name:</label><input type="text" id="starName"></input> <br><label for="starId">Star ID:</label><input type="text" id="starId"></input> <br><br><button id="createStar" onclick="App.createStar()">Create Star</button> <br><br> <br> <span id="status"></span> <br> <script src="index.js"></script> </body> </html>
Time to test it !
I changed the code in the truffle config, exactly as in the previous version of the Dapp. and then :
To run your application you will need to:
- Run
truffle develop
orsudo truffle develop
commands. - Run
compile
command. - Run
migrate --reset
Then
- Open a second terminal window, and make sure you are inside your project directory.
- Run
cd app
to move inside the app folder. - Run
npm run dev
command.
This will start our port at 8080. Open http://localhost:8080/ in your browser. Make sure the Metamask extension is installed, and you are logged into it, and also have imported the accounts.
So I had an error when I tried interacting with the smart contract, the error was “the tx doesn’t have the correct nonce. account has nonce of:”
All I had to do to correct the error was to reset my account in my metamask, here :
it clears out the history of transactions and solved the error when I tried again.