This article intends to provide a full-blown guide on how to code and deploy a single-page Ethereum-powered ICO (Initial Coin Offering) web application. This is how the final result looks like.
We will use Ubuntu operating system and go through all the steps, from blank page to deployment onto the Ethereum Ropsten network.
I see many guides around describe only bits and pieces of the Ethereum development process. I felt something that described all the steps in detail was missing. Also, this guide is very suitable for those like me who have quite out-dated hardware and want to use a “light” architecture.
1. Get yourself ready
We will begin with installing on our machine all the software needed for the deployment.
Install git, node and npm
Firstly, we will need to install git (a version control system), nodejs (the run-time environment for executing JavaScript code server-side) and npm, the node package manager.
Open the terminal (CTRL + T) and type the below commands in sequence.
sudo apt-get upgrade
sudo apt-get install -y build-essential
sudo apt-get install -y git
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
sudo apt-get install -y nodejs
(Optional) Run the below to verify that the installation was successful
node -v # should return the node version
npm -v # should return the npm version
Install Truffle
We will then install Truffle, one of the most popular development frameworks for Ethereum. Note that the flag -g makes the installation global (which is quite important).
Again the command
should confirm that everything went well (as of the time of writing, the latest Truffle version 4.0.4 is highly recommended)
Install Metamask
Assuming that Google Chrome (recommended for this project) is installed on your system, we will now proceed to download and install Metamask Chrome extension.
Metamask is a bridge that allows you run Ethereum dApps right in your browser, without running a full Ethereum node. Essentially, instead than connecting directly to (and downloading the full) blockchain, the Metamask bridge connects for you to the blockchain as a “light” interface.
It is extremely simple to create an Ethereum wallet in Metamask, but for further reference you may want to watch this video.
During the new wallet creation procedure, you will be provided with a 12 word mnemonic. Make sure you write these down somewhere safe as they will be: a) your only way to recover the wallet, b) needed during the deployment process.
Register to Infura
Another step needed is to register to Infura.
After leaving your details to Infura, please take note of the API key. Infura is a service that provides secure, reliable, and scalable access to the Ethereum blockchain. Infura registration is required because Metamask cannot deploy smart contracts directly, but think about the two services as closely related.
Register and Install Heroku
Finally, we will start getting ready for deployment by creating an account on Heroku. Heroku is a platform as a service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud. It is free to register and please take note of the username and password.
After the registration, please download the Heroku client running the commands below.
curl -L https://cli-assets.heroku.com/apt/release.key | sudo apt-key add -
sudo apt-get update
sudo apt-get install heroku
2. Prepare the environment
We will start by unleashing all the power of Truffle. The guys at Truffle have provided us with a few nice templates. We will use the “Truffle box” based on Webpack.
Create a new folder to contain the project and cd into it:
cd myfirstdapp
and run
Please give a few seconds for the box to unpack, and then get yourself familiar with the directory tree structure. There is another package that is not provided “out of the box” (no pun intended) and that needs to be installed running the below:
At this stage feel free to delete all the unnecessary boilerplate files from the truffle box, as per below:
rm /contracts/Metacoin.sol
rm /migrations/2_deploy_contracts.js
rm /test/metacoin.js
rm /test/TestMetacoin.sol
3. Begin coding
To launch an ICO you would generally need:
- two Ethereum contracts (a Token and a Fundraise contract)
- a front/back-end infrastructure for your app and your users to interact with the contract (an html, a css and a javascript files)
In our example we are adding a third Ethereum contract, a Fund that will collect the proceeds of the Fundraise. This is just for demonstrative purpose.
The Smart Contracts
Below is the solidity code of the (very basic) Token. Copy/paste this code and save it in a file Token.sol in the /contracts folder.
It would be ideal if you tried to understand what this code does. Please refer to the Solidity documentation, the Ethereum token creation page and the ERC20 token standard if you are unsure.
Token.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | pragma solidity ^0.4.18; contract Token { // Storage Variables string public name; string public symbol; uint256 public decimals; uint256 public totalSupply; mapping (address => uint256) public balances; function Token (string _name, string _symbol, uint256 _decimals, uint256 _initialSupply) public { name = _name; symbol = _symbol; decimals = _decimals; totalSupply = _initialSupply * 10 ** decimals; balances[msg.sender] = totalSupply; } // Fallback function // Do not accept ethers function() public { // This will throw an exception - in effect no one can purchase the coin assert(true == false); } // Getters function totalSupply() public constant returns (uint) { return totalSupply; } function name() public constant returns (string) { return name; } function symbol() public constant returns (string) { return symbol; } function decimals() public constant returns (uint256) { return decimals; } // Real Utility functions function balanceOf(address _owner) public constant returns (uint256) { return balances[_owner]; } function transfer(address _to, uint256 _value) public returns (bool) { // Check basic conditions require(balances[msg.sender] >= _value); require(_to != 0x0); require(_value > 0); // Another check for assertion uint previousBalances = balances[msg.sender] + balances[_to]; // Executes the transfer balances[msg.sender] -= _value; balances[_to] += _value; // Assert that the total of before is equal to the total of now assert(balances[msg.sender] + balances[_to] == previousBalances); return true; } } |
Below is the code for the Fundraise, to be named Fundraise.sol and saved again in the /contracts folder.
Fundraise.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | pragma solidity ^0.4.18; interface token { function transfer(address to, uint256 value); } contract Fundraise { /* Storage Variables - remain in memory */ // Keep track of the fundraising timeline bool public fundraiseWasStarted = false; bool public fundraiseIsOpen = false; uint256 public deadline; // Keep track of the fundraising target in Ether uint256 public minimumTargetInWei; bool public targetReached = false; // Keep track of the amount raised so far uint256 public amountRaisedSoFarInWei = 0; // Token used as a reward for the fundraise // and conversion price token public tokenToBeUsedAsReward; uint256 public priceOfTokenInWei; // Fundraise beneficiary and owner address public fundraiseBeneficiary; address public fundraiseOwner; // Keep track of the Ether balances of contributors mapping(address => uint256) public balances; // Constructor function Fundraise() public { fundraiseOwner = msg.sender; } // Modifiers // Function that can be actioned only by the owner modifier onlyOwner { require(msg.sender == fundraiseOwner); _; } // Function that can be actioned only when the fundRaise // was started and closed modifier fundraiseWasStartedAndDoesNotMatterTheRest() { require(fundraiseWasStarted == true); _; } modifier fundraiseWasStartedAndNowClosed() { require(fundraiseIsOpen == false && fundraiseWasStarted == true); _; } modifier fundraiseWasStartedAndStillOpen() { require(fundraiseIsOpen == true && fundraiseWasStarted == true); _; } modifier fundraiseWasNotYetStarted() { require(fundraiseIsOpen == false && fundraiseWasStarted == false); _; } // Opens the fundraise, only if it was not so before function openFundraise( uint256 _timeOpenInMinutes, uint256 _minimumTargetInEthers, address _tokenToBeUsedAsReward, uint256 _priceOfTokenInEther, address _fundraiseBeneficiary ) public onlyOwner fundraiseWasNotYetStarted { fundraiseIsOpen = true; fundraiseWasStarted = true; deadline = now + (_timeOpenInMinutes * 1 minutes); minimumTargetInWei = _minimumTargetInEthers * 1 ether; tokenToBeUsedAsReward = token(_tokenToBeUsedAsReward); priceOfTokenInWei = _priceOfTokenInEther * 1 ether; fundraiseBeneficiary = _fundraiseBeneficiary; } // At any time can check the status of fundraise function checkStatusOfFundraise() public fundraiseWasStartedAndDoesNotMatterTheRest { if (now >= deadline) { fundraiseIsOpen = false; } if (amountRaisedSoFarInWei >= minimumTargetInWei) { targetReached = true; } } function contributeToFundraise() public payable fundraiseWasStartedAndStillOpen { uint amount = msg.value; balances[msg.sender] += amount; amountRaisedSoFarInWei += amount; tokenToBeUsedAsReward.transfer(msg.sender, amount / priceOfTokenInWei); } function withdraw() public payable fundraiseWasStartedAndNowClosed { if (targetReached == false) { uint amount = balances[msg.sender]; balances[msg.sender] = 0; if (msg.sender.send(amount)) { tokenToBeUsedAsReward.transfer(fundraiseOwner, amount / priceOfTokenInWei); } } else if (targetReached == true && fundraiseBeneficiary == msg.sender) { if (fundraiseBeneficiary.send(amountRaisedSoFarInWei)){ } else { targetReached = false; } } } } |
For the Fund the code is much simpler (almost an empty template). Copy/paste and save in Fund.sol in the /contracts folder. Note: do not run this on the live Ethereum network as I have not implemented a way to withdraw the Ether from the Fund (on purpose).
Fund.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | pragma solidity ^0.4.18; interface fundraise { function withdraw(); } contract Fund { uint256 public balanceInWei; string public fundName; address public fundOwner; function Fund(string _name) public { fundName = _name; fundOwner = msg.sender; } // Modifiers // Function that can be actioned only by the owner modifier onlyOwner { require(msg.sender == fundOwner); _; } function () public payable { } function fundName() public constant returns (string) { return fundName; } function callWithdraw(address _currentFundraise) onlyOwner payable { fundraise(_currentFundraise).withdraw(); } function getBalance() public returns (uint256) { balanceInWei = this.balance; return balanceInWei; } } |
The Front-end
The front-end – which is modelled upon the front-end framework Bootstrap – will be defined in the index.html file (placed in the app folder) as per below. Please feel free to do some customisation.
Note that it is explicitly designed to handle connection to the Ropsten testnet. Any attempt to deploy on mainnet would require some further customisation.
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | <!DOCTYPE html> <html> <head> <title>Fundraise DApp</title> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous"> <script src="app.js"></script> </head> <body> <!-- Image and text --> <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top"> <div class="container"> <a class="navbar-brand" href="http://www.massimilianoterzi.it"> <img src="./images/massi.png" width="30" height="30" class="d-inline-block align-top" alt="">   Massi's Fundraise Dapp </a> </div> </nav> <main role="main" class="container"> <div class="alert alert-info" role="alert">You are connected on <span id="identifiedNet"></span> network</div> <div class="card-deck"> <div class="card" style="width: 20rem"> <div class="card-header"> Your Current Account </div> <div class="card-body"> <p class="card-text">Ether Balance: <span id="youretherbalance0"></span></p> <p class="card-text">Your Token Balance: <span id="yourtokenbalance0"></span> (<span id="tokenpercentage0"></span>%)</p> </div> <div class="card-footer"> <small class="text-muted">Your Address: <a target="_blank" title="Check on Etherscan" id="youraddress0link" href=""><span id="youraddress0"></span></a></small> </div> </div> </div> <br> <!-- Example single danger button --> <div class="btn-group"> <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Open Fundraise </button> <div class="dropdown-menu" style="width: 25rem" aria-labelledby="dLabel"> <div class="px-4 py-3"> <div class="form-group form-inline"> <label for="OpenFundraiseFormMinimumTarget">Minimum Target: </label> <input type="text" class="form-control" id="OpenFundraiseFormMinimumTarget" placeholder="Min. Target (in Ether)" required> </div> <div class="form-group form-inline"> <label for="OpenFundraiseFormDeadline">Time Open: </label> <input type="text" class="form-control" id="OpenFundraiseFormDeadline" placeholder="Time Open (In Minutes)" required> </div> <div class="form-group form-inline"> <label for="OpenFundraiseFormPriceOfToken">Price of Token (in Ether): </label> <input type="text" class="form-control" id="OpenFundraiseFormPriceOfToken" placeholder="Price of Token (In Ether)" required> </div> <div class="form-group form-inline"> <label for="OpenFundraiseFormBeneficiary">Beneficiary Address: </label> <input type="text" class="form-control" id="OpenFundraiseFormBeneficiary" placeholder="Beneficiary Address" required> </div> <div class="form-group form-inline"> <label for="OpenFundraiseFormTokenReward">Token Address: </label> <input type="text" class="form-control" id="OpenFundraiseFormTokenReward" placeholder="Token Address" required> </div> <button class="btn btn-primary" onclick="App.launchFundraise()">Launch</button> </div> </div> </div> <!-- Example single danger button --> <div class="btn-group"> <button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> Contribute </button> <div class="dropdown-menu" style="width: 25rem" aria-labelledby="dLabel"> <div class="px-4 py-3"> <div class="form-group form-inline"> <label for="ContributeDropdownFormAmount">Amount</label> <input type="text" class="form-control" id="ContributeDropdownFormAmount" placeholder="Amount (in Ether)"> </div> <button class="btn btn-primary" onclick="App.contributeToFundraiseApp()">Contribute</button> </div> </div> </div> <!-- Example single danger button --> <div class="btn-group"> <button class="btn btn-info" onclick="App.checkFundraiseStatus()"> Check Fundraise Status </button> </div> <!-- Example single danger button --> <div class="btn-group"> <button class="btn btn-warning" onclick="App.withdrawAmountsApp()"> Withdraw </button> </div> <br> <br> <div class="card-deck"> <div class="card"> <div class="card-header"> Token </div> <div class="card-body"> <p class="card-text">Token Name: <span id="tokenname"></span></p> <p class="card-text">Token Symbol: <span id="tokensymbol"></span></p> <p class="card-text">Token Decimals: <span id="tokendecimals"></span></p> <p class="card-text">Token Supply: <span id="tokentotalsupply"></span></p> </div> <div class="card-footer"> <small class="text-muted">Token Address: <a target="_blank" title="Check on Etherscan" id="tokenaddresslink" href=""><span id="tokenaddress"></span></a></small> </div> </div> <div class="card"> <div class="card-header"> Fundraise </div> <div class="card-body"> <p class="card-text">Started: <span id="hasfundraisestarted"></span></p> <p class="card-text">Token Balance: <span id="fundraisetokenbalance"></span></p> <p class="card-text">Ether Balance: <span id="fundraiseetherbalance"></span></p> <p class="card-text">Target: <span id="fundraisetarget"></span></p> <p class="card-text">Deadline: <span id="fundraisedeadline"></span></p> <p class="card-text">Open: <span id="isfundraiseopen"></span></p> <p class="card-text">Target Reached: <span id="isfundraisetargetreached"></span></p> </div> <div class="card-footer"> <small class="text-muted">FundRaise Address: <a target="_blank" title="Check on Etherscan" id="fundraiseaddresslink" href=""><span id="fundraiseaddress"></span></a></small> </div> </div> <div class="card"> <div class="card-header"> Fund </div> <div class="card-body"> <p class="card-text">Fund Name: <span id="fundname"></span></p> <p class="card-text">Fund EtherBalance: <span id="fundetherbalance"></span></p> </div> <div class="card-footer"> <small class="text-muted">Fund Address: <a target="_blank" title="Check on Etherscan" id="fundaddresslink" href=""><span id="fundaddress"></span></a></small> </div> </div> </div> <br> </main> <footer class="footer fixed-bottom"> <div class="container"> <span class="text-muted">© 2018, Massimiliano Terzi</span> </div> </footer> </body> <script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script> </html> |
The Back-end
The back-end is provided below. It is a javascript file that has a number of tasks:
- It connects to the Ethereum web3 provider (Metamask in our case)
- It connects the deployed contracts to the graphical front-end interface
- It refreshes the UI every time there is a change or an interaction with the blockchain
The file needs to be (re)-placed in the app/javascript directory.
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 | // Import the page's CSS. Webpack will know what to do with it. import "../stylesheets/app.css"; // Import libraries we need. import { default as Web3} from 'web3'; import { default as contract } from 'truffle-contract' // Import our contract artifacts and turn them into usable abstractions. import massitoken_artifacts from '../../build/contracts/Token.json' import fund_artifacts from '../../build/contracts/Fund.json' import fundraise_artifacts from '../../build/contracts/Fundraise.json' // MetaCoin is our usable abstraction, which we'll use through the code below. var MassiToken = contract(massitoken_artifacts); var Fund = contract(fund_artifacts); var Fundraise = contract(fundraise_artifacts); // The following code is simple to show off interacting with your contracts. // As your needs grow you will likely need to change its form and structure. // For application bootstrapping, check out window.addEventListener below. var accounts; var account; var account1; window.App = { start: function() { var self = this; // Bootstrap the MetaCoin abstraction for Use. MassiToken.setProvider(web3.currentProvider); Fund.setProvider(web3.currentProvider); Fundraise.setProvider(web3.currentProvider); var identifiedNetwork; web3.version.getNetwork((err, netId) => { switch (netId) { case "1": identifiedNetwork = "Mainnet"; console.log('This is mainnet'); break; case "2": identifiedNetwork = "Morden"; console.log('This is the deprecated Morden test network.'); break; case "3": identifiedNetwork = "Ropsten"; console.log('This is the ropsten test network.'); break; case "4": identifiedNetwork = "Rinkeby"; console.log('This is the Rinkeby test network.'); break; case "42": identifiedNetwork = "Kovan"; console.log('This is the Kovan test network.'); break; default: identifiedNetwork = "Unknown"; console.log('This is an unknown network.'); } document.getElementById("identifiedNet").innerHTML = identifiedNetwork; }); // Get the initial account balance so it can be displayed. web3.eth.getAccounts(function(err, accs) { if (err != null) { alert("There was an error fetching your accounts."); return; } if (accs.length == 0) { alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly."); return; } accounts = accs; account = accounts[0]; // account1 = accounts[1]; var accountInterval = setInterval(function() { if (web3.eth.accounts[0] !== account) { account = web3.eth.accounts[0]; self.updateInterface(); } }, 5000); document.getElementById("youraddress0").innerHTML = account; $("#youraddress0link").attr("href", "https://ropsten.etherscan.io/address/" + account); // document.getElementById("youraddress1").innerHTML = account1; web3.eth.getBalance(account, function(err,result){ if(err){ console.log(err); console.log("Error getting balance"); } else{ document.getElementById("youretherbalance0").innerHTML = web3.fromWei(result,'ether'); } }); // web3.eth.getBalance(account1, function(err,result){ // if(err){ // console.log(err); // console.log("Error getting balance"); // } else{ // document.getElementById("youretherbalance1").innerHTML = web3.fromWei(result,'ether'); // } // }); self.updateInterface(); }); }, updateInterface: function() { var self = this; self.getTokenAddress(); self.getFundraiseAddress(); self.getFundAddress(); self.getTokenName(); self.getFundName(); self.getTokenTotalSupply(); self.getTokenSymbol(); self.getTokenDecimals(); self.getPercentageOwned(); self.getFundEtherBalance(); self.getFundraiseEtherBalance(); self.getFundraiseStatus(); self.getFundraiseTarget(); self.getFundraiseisStarted(); self.getFundraiseisTargetReached(); self.getFundraiseDeadline(); self.refreshTokenBalanceAccounts(); self.refreshTokenBalanceFundraise(); }, refreshTokenBalanceAccounts: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.balanceOf.call(account, {from: account}); }).then(function(value) { var balance_element = document.getElementById("yourtokenbalance0"); balance_element.innerHTML = value.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting balance; see log 1."); }); // MassiToken.deployed().then(function(instance) { // token = instance; // return token.balanceOf.call(account1, {from: account1}); // }).then(function(value) { // var balance_element1 = document.getElementById("yourtokenbalance1"); // balance_element1.innerHTML = value.valueOf(); // }).catch(function(e) { // console.log(e); // console.log("Error getting balance; see log 2."); // }); }, refreshTokenBalanceFundraise: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; var fundraiseaddress = document.getElementById("fundraiseaddress").innerHTML; return token.balanceOf(fundraiseaddress); }).then(function(tokenbalancefundraise) { var balance_element2 = document.getElementById("fundraisetokenbalance"); balance_element2.innerHTML = tokenbalancefundraise.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting balance; see log 3."); }); }, getTokenAddress: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.address; }).then(function(address) { var address_element = document.getElementById("tokenaddress"); address_element.innerHTML = address.valueOf(); var contribution_element = document.getElementById("OpenFundraiseFormTokenReward"); contribution_element.value = address.valueOf(); $("#tokenaddresslink").attr("href", "https://ropsten.etherscan.io/address/" + address.valueOf()); }).catch(function(e) { console.log(e); console.log("Error getting address; see log."); }); }, getFundraiseAddress: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.address; }).then(function(address) { var fundraise_address_element = document.getElementById("fundraiseaddress"); fundraise_address_element.innerHTML = address.valueOf(); $("#fundraiseaddresslink").attr("href", "https://ropsten.etherscan.io/address/" + address.valueOf()); }).catch(function(e) { console.log(e); console.log("Error getting address; see log."); }); }, getFundraiseTarget: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.minimumTargetInWei.call(); }).then(function(mintarget) { var fundraise_target_element = document.getElementById("fundraisetarget"); fundraise_target_element.innerHTML = web3.fromWei(mintarget.valueOf()); }).catch(function(e) { console.log(e); console.log("Error getting fundraising target; see log."); }); }, getFundraiseStatus: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.fundraiseIsOpen.call(); }).then(function(fundraisestatus) { var status_element = document.getElementById('isfundraiseopen'); status_element.innerHTML = fundraisestatus.toString(); }).catch(function(e) { console.log(e); console.log("Error getting fundraise status; see log."); }); }, getFundraiseisStarted: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.fundraiseWasStarted.call(); }).then(function(started) { var started_element = document.getElementById('hasfundraisestarted'); started_element.innerHTML = started.toString(); }).catch(function(e) { console.log(e); console.log("Error getting whether fundraise was started; see log."); }); }, getFundraiseisTargetReached: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.targetReached.call(); }).then(function(targetreach) { var target_reach_element = document.getElementById('isfundraisetargetreached'); target_reach_element.innerHTML = targetreach.toString(); }).catch(function(e) { console.log(e); console.log("Error getting whether fundraise target was reached; see log."); }); }, getFundAddress: function() { var self = this; var fund; Fund.deployed().then(function(instance) { fund = instance; return fund.address; }).then(function(address) { var fund_address_element = document.getElementById("fundaddress"); fund_address_element.innerHTML = address.valueOf(); var contribution_element = document.getElementById("OpenFundraiseFormBeneficiary"); contribution_element.value = address.valueOf(); $("#fundaddresslink").attr("href", "https://ropsten.etherscan.io/address/" + address.valueOf()); }).catch(function(e) { console.log(e); console.log("Error getting address; see log."); }); }, getFundName: function() { var self = this; var fund; Fund.deployed().then(function(instance) { fund = instance; return fund.fundName.call(); }).then(function(ilnome) { var fund_name_element = document.getElementById("fundname"); fund_name_element.innerHTML = ilnome.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting fund name; see log."); }); }, getFundraiseDeadline: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; return fundraise.deadline.call(); }).then(function(deadline) { var newDate = new Date(); newDate.setTime(parseInt(deadline)*1000); var dateString = newDate.toUTCString(); var deadline_element = document.getElementById('fundraisedeadline'); deadline_element.innerHTML = dateString; }).catch(function(e) { console.log(e); console.log("Error getting fundraise deadline; see log."); }); }, getTokenTotalSupply: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.totalSupply.call(); }).then(function(supply) { var totalsupply_element = document.getElementById("tokentotalsupply"); totalsupply_element.innerHTML = supply.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting token total supply; see log."); }); }, getTokenName: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.name.call(); }).then(function(name) { var name_element = document.getElementById("tokenname"); name_element.innerHTML = name.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting token name; see log."); }); }, getTokenSymbol: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.symbol.call(); }).then(function(symbol) { var symbol_element = document.getElementById("tokensymbol"); symbol_element.innerHTML = symbol.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting token symbol; see log."); }); }, getTokenDecimals: function() { var self = this; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.decimals.call(); }).then(function(decimals) { var decimals_element = document.getElementById("tokendecimals"); decimals_element.innerHTML = decimals.valueOf(); }).catch(function(e) { console.log(e); console.log("Error getting token decimals; see log."); }); }, getFundEtherBalance: function() { var self = this; var fund; Fund.deployed().then(function(instance) { fund = instance; web3.eth.getBalance(fund.address, function(error,result){ if(error){ console.log(error); } else { var balance_element = document.getElementById("fundetherbalance"); var balanceinEther = web3.fromWei(result, 'ether'); balance_element.innerHTML = balanceinEther.valueOf(); } }); }); }, getFundraiseEtherBalance: function() { var self = this; var fundraise; Fundraise.deployed().then(function(instance) { fundraise = instance; web3.eth.getBalance(fundraise.address, function(error,result){ if(error){ console.log(error); } else { var balance_element = document.getElementById("fundraiseetherbalance"); var balanceinEther = web3.fromWei(result, 'ether'); balance_element.innerHTML = balanceinEther.valueOf(); } }); }); }, getPercentageOwned: function() { var self = this; let balanceofcurrentaddress0; let balanceofcurrentaddress1; var token; MassiToken.deployed().then(function(instance) { token = instance; return token.balanceOf.call(account, {from: account}); }).then(function(value) { balanceofcurrentaddress0 = value.valueOf(); return token.totalSupply.call(); }).then(function(supply) { var totalSupply = supply.valueOf(); var percentage = balanceofcurrentaddress0/totalSupply * 100; document.getElementById("tokenpercentage0").innerHTML = percentage; }).catch(function(e) { console.log(e); console.log("Error getting percentage; see log."); }); }, sendCoin: function() { var self = this; var amount = parseInt(document.getElementById("amount").value); var receiver = document.getElementById("receiver").value; console.log("Initiating transaction... (please wait)"); var meta; MetaCoin.deployed().then(function(instance) { meta = instance; return meta.sendCoin(receiver, amount, {from: account}); }).then(function() { console.log("Transaction complete!"); self.refreshBalance(); }).catch(function(e) { console.log(e); console.log("Error sending coin; see log."); }); }, launchFundraise: function(){ var self = this; var target = parseInt(document.getElementById('OpenFundraiseFormMinimumTarget').value); var timeopeninminutes = parseInt(document.getElementById('OpenFundraiseFormDeadline').value); var priceoftokeninether = parseInt(document.getElementById('OpenFundraiseFormPriceOfToken').value); var tokentobeusedasreward = document.getElementById('OpenFundraiseFormTokenReward').value; var fundraisebeneficiary = document.getElementById('OpenFundraiseFormBeneficiary').value; var fundraise; Fundraise.deployed().then(function (instance) { fundraise = instance; return fundraise.openFundraise(timeopeninminutes, target, tokentobeusedasreward, priceoftokeninether, fundraisebeneficiary, {from: account, gas: 1000000}); }).then(function() { console.log("Fundraise Launch successful"); self.getFundraiseStatus(); self.getFundraiseisStarted(); self.getFundraiseTarget(); self.getFundraiseDeadline(); self.getFundraiseisTargetReached(); }).catch(function(e) { console.log(e); console.log("Error launching Fundraise; see log."); }); }, contributeToFundraiseApp: function(){ var self = this; var fundraise; Fundraise.deployed().then(function (instance) { fundraise = instance; return web3.toWei(parseInt(document.getElementById('ContributeDropdownFormAmount').value),'ether'); }).then(function(contrib){ return fundraise.contributeToFundraise({from: web3.eth.accounts[0], value: contrib, gas: 1000000}); }).then(function() { console.log("Contribution Successful"); self.refreshTokenBalanceAccounts(); self.refreshTokenBalanceFundraise(); self.getFundraiseEtherBalance(); }).catch(function(e) { console.log(e); console.log("Error contributing to Fundraise; see log."); }); }, withdrawAmountsApp: function(){ var self = this; var fundraiseaddress = document.getElementById("fundraiseaddress").innerHTML; var fund; Fund.deployed().then(function (instance) { fund = instance; return fund.callWithdraw(fundraiseaddress, {from: account}); }).then(function() { console.log("Success in withdrawing amount"); self.getFundEtherBalance(); self.getFundraiseEtherBalance(); self.refreshTokenBalanceFundraise(); }).catch(function(e) { console.log(e); console.log("Error withdrawing amounts; see log."); }); }, checkFundraiseStatus: function(){ var self = this; var fundraise; Fundraise.deployed().then(function (instance) { fundraise = instance; return fundraise.checkStatusOfFundraise({from: account}); }).then(function() { console.log("Success in checking fundraise status"); self.getFundraiseisTargetReached(); self.getFundraiseStatus(); }).catch(function(e) { console.log(e); console.log("Error checking Fundraise status; see log."); }); } }; window.addEventListener('load', function() { // Checking if Web3 has been injected by the browser (Mist/MetaMask) if (typeof web3 !== 'undefined') { console.warn("Using web3 detected from external source. If you find that your accounts don't appear or you have 0 MetaCoin, ensure you've configured that source properly. If using MetaMask, see the following link. Feel free to delete this warning. :) http://truffleframework.com/tutorials/truffle-and-metamask") // Use Mist/MetaMask's provider window.web3 = new Web3(web3.currentProvider); } else { console.warn("No web3 detected. Falling back to http://127.0.0.1:9545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask"); // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail) window.web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:9545")); } App.start(); }); |
4. Wrap everything up
The Blockchain Deployment
In Truffle, the process of deploying the smart contracts to the Ethereum blockchain is called migration. The word confused me at first, but consider it part of the jargon. To migrate your contracts onto the blockchain, you need to adjust the configurations of two files.
The first has to be named 2_deploy_contracts.js and contains the parameters that will be attributed to the creation of the contracts. Copy/paste the code below.
This file:
- Deploys a token called “MassiToken”, with symbol “MAS”, zero decimals and an initial supply of 1,000 MAS
- Deploys a fund called “MassiFund”
- Deploys a fundraise. The parameters of the fundraise will be specified by the user in the front-end
2_deploy_contracts.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | var Token = artifacts.require("./Token.sol"); var Fund = artifacts.require("./Fund.sol"); var Fundraise = artifacts.require("./Fundraise.sol") module.exports = function(deployer) { deployer.deploy(Token, "MassiToken", "MAS", 0, 1000); var baseAccount; var accounts; web3.eth.getAccounts(function(err, accs) { if (err != null) { alert("There was an error fetching your accounts."); return; } if (accs.length == 0) { alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly."); return; } accounts = accs; baseAccount = accounts[0]; deployer.deploy(Fund, "MassiFund"); deployer.deploy(Fundraise, baseAccount); }); }; |
The second file is the configuration for Truffle and instructs where to deploy. The custom file contains only the “development” configuration. Therefore, in order to work on the Ropsten network, it has to be customised as per below.
Do not forget to edit the mnemonic (your 12 words Metamask seed) and the infura_api_key (you should have gotten this when you registered on Infura.
truffle.js
var mnemonic = "YOUR 12 WORD MNEMONIC PASTE IT HERE";
var infura_api_key = "YOUR INFURA KEY GOES HERE"
// Allows us to use ES6 in our migrations and tests.
require('babel-register')
module.exports = {
networks: {
development: {
host: '127.0.0.1',
port: 7545,
network_id: '*' // Match any network id
},
ropsten: {
provider: new HDWalletProvider(mnemonic, "https://ropsten.infura.io/" + infura_api_key),
network_id: 3,
gas: 4600000
}
}
}
At this point, all the ingredients are ready to compile the contracts and migrate them on the blockchain.
Run the code below in the terminal window.
truffle migrate --reset --network ropsten # migrates the contracts on Ropsten
Nonetheless, if you still feel unsure and would like to try to see if everything works on a local network, please run the following
> compile # compiles the contracts
> migrate --reset --network development # migrates the contracts on the local development blockchain
The Web Deployment
The web deployment on Heroku is not a process without potential issues and a number of files will have to customised (i.e. they are obtained with the Truffle webpack but have to be edited to make them work in deployment).
Please edit the webpack.config.js file as per below (in particular, please notice the devServer string which is not part of the original configuration.
webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './app/javascripts/app.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'app.js'
},
plugins: [
// Copy our app's index.html to the build folder.
new CopyWebpackPlugin([
{ from: './app/index.html', to: "index.html" },
{ from: './app/images', to: "images" }
])
],
module: {
rules: [
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
],
loaders: [
{ test: /\.json$/, use: 'json-loader' },
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
query: {
presets: ['es2015'],
plugins: ['transform-runtime']
}
}
]
},
devServer: {
port: process.env.PORT || 8080,
host: '0.0.0.0',
disableHostCheck: true
}
}
The package.json file also needs to be tweaked: Heroku does not install devDependencies out of the box. The easiest shortcut (adopted below) is to move all the devDependencies into Dependencies so they are installed automatically by Heroku.
package.json
"name": "truffle-init-webpack",
"version": "0.0.2",
"description": "Frontend example using truffle v3",
"scripts": {
"lint": "eslint ./",
"build": "webpack",
"dev": "webpack-dev-server"
},
"author": "Douglas von Kohorn",
"license": "MIT",
"dependencies": {
"babel-cli": "^6.22.2",
"babel-core": "^6.22.1",
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.1.8",
"babel-preset-es2015": "^6.22.0",
"babel-register": "^6.22.0",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.26.1",
"eslint": "^3.14.0",
"eslint-config-standard": "^6.0.0",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-mocha": "^4.8.0",
"eslint-plugin-promise": "^3.0.0",
"eslint-plugin-standard": "^2.0.0",
"html-webpack-plugin": "^2.28.0",
"json-loader": "^0.5.4",
"style-loader": "^0.13.1",
"truffle-contract": "^1.1.11",
"truffle-hdwallet-provider": "0.0.3",
"web3": "^0.20.0",
"webpack": "^2.2.1",
"webpack-dev-server": "^2.3.0"
}
}
One of the latest steps before being able to deploy to the web is to add a so-called Procfile in the app main directory. This file represents the first command that gets executed by Heroku when someone tries to access the website online. In our case, it’s a one-liner.
Procfile
Deploy the app to Heroku
First run
and enter your credentials to log-in (username and password that you used for Heroku registration).
Then initialize a git repository and push the code onto Heroku. Run:
git add .
git commit -m "Added a Procfile"
heroku create
git push heroku master
(Optional) You can add a .gitignore file in the app main directory. The gitignore does exactly what it’s called: all the files or directories specified in the gitignore will not be deployed onto Heroku.
5. Play with your newly create Dapp (Decentralized Application)
Once the Dapp is deployed on the Ropsten Ethereum network and the web, you can start playing with it.
- Launch a fundraise with a push of a button (need to set a target, the price for your token, and a deadline in minutes)
- Contribute some of your token to the fundraise so that it can be used as a reward for those who will invest Ether (usually you would not want to contribute >50% of your issued tokens, read why here
- Check the fundraise status
- Close the fundraise and have the fund withdraw the contributed Ether
Note that this code and example is purely intended as an illustrative example to learn the Ethereum deployment steps. It should not be used (in part or in whole) in a real ICO.
Feel free to write me if you have questions, spot some bugs or would just like to contribute to the article.
Would also be happy to take Ether donations to: 0x5456068c000073478969b98086bECB79159cED66