├── .nojekyll ├── README.md ├── contracts ├── BasicToken.sol ├── ERC20.sol ├── ERC20Basic.sol ├── ExampleToken.sol ├── SafeMath.sol └── StandardToken.sol ├── mobile ├── app │ ├── main.json │ └── simple.json ├── qr │ ├── privatekey.json │ ├── publickey.json │ └── scanner.json ├── tutorial.md └── wallet │ ├── wallet.html │ └── wallet.json └── web ├── index.html └── tutorial.md /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gliechtenstein/erc20/605de1442fcbca38f3d973eaf967bde66994c37b/.nojekyll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack ERC20 App 2 | 3 | This repository contains EVERYTHING. 4 | 5 | 1. Backend: ERC20 contract written in solidity 6 | 2. Web Frontend: Web3.js DApp. 7 | 3. Mobile Frontend: JSON markup for [Jasonette](https://www.jasonette.com) 8 | 9 | This document will explain how these all come together, and walk you through everything you need to do to deploy a contract to Ethereum, build a web app, and build a mobile app. 10 | 11 | # 1. Backend 12 | 13 | The "backend" is just a set of solidity contracts. This tutorial is not about writing ERC20 tokens or smart contracts so we're just going to use the most basic token possible. 14 | 15 | ![zeppelin](https://openzeppelin.org/img/logo-zeppelin.png) 16 | 17 | 99% of it is copy and pasted from Openzeppelin's [Zeppelin-solidity](https://github.com/OpenZeppelin/zeppelin-solidity) repository. The following code are 100% copy and pastes: 18 | 19 | - [BasicToken.sol](contracts/BasicToken.sol) 20 | - [ERC20.sol](contracts/ERC20.sol) 21 | - [ERC20Basic.sol](contracts/ERC20Basic.sol) 22 | - [SafeMath.sol](contracts/SafeMath.sol) 23 | - [StandardToken.sol](contracts/StandardToken.sol) 24 | 25 | 1% of it is customization: 26 | 27 | - [ExampleToken.sol](contracts/ExampleToken.sol) 28 | 29 | The `ExampleToken` is the only contract that I've tweaked, just to customize the symbol, decimals, and total supply 30 | 31 | For simplicity, I didn't use any frameworks like [Truffle](https://github.com/trufflesuite/truffle). 32 | 33 | ## Deployment 34 | 35 | You can go to [Remix](https://remix.ethereum.org), copy and paste all the files in [contracts](contracts) folder, and deploy to Ethereum. 36 | 37 | There are many tutorials on how to do this online. Google them. 38 | 39 | In my case, I have deployed it to [rinkeby](https://www.rinkeby.io/#stats) testnet, and the resulting contract address is [0x3823619872186eff668aad8192590faaffef6a5f](https://rinkeby.etherscan.io/address/0x3823619872186eff668aad8192590faaffef6a5f). 40 | 41 | We will use [this contract address in the DApp](https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L190) to connect to the contract. 42 | 43 | --- 44 | 45 | # 2. Web 46 | 47 | The entire DApp is a single HTML file. It uses a JavaScript app framework called [cell.js](https://www.celljs.org) to implement the app in as simple manner as possible. 48 | 49 | **You can try it out here: https://gliechtenstein.github.io/erc20/web/** 50 | 51 | ![img](https://gliechtenstein.github.io/images/dapp.png) 52 | 53 | Note that: 54 | 55 | - For simplicity, only [two external libraries](https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L38) are used: [web3.js](https://github.com/ethereum/web3.js/) and [cell.js](https://github.com/intercellular/cell) 56 | - You can see the entire code transparently (no compilation, etc.) when you ["view source" from the web app](https://gliechtenstein.github.io/erc20/web/) 57 | - The app uses version 0.14.0 (not ver 1.0+) of web3.js library 58 | 59 | Learn more about the web frontend: [Web app tutorial](web/tutorial.md) 60 | 61 | 62 | # 3. Mobile 63 | 64 | The mobile frontend is powered by [Jasonette](https://www.jasonette.com), a markup driven approach to building mobile apps. 65 | 66 | ![img](https://gliechtenstein.github.io/images/erc20.gif) 67 | 68 | Jasonette is view-oriented. Therefore everything revolves around views. There are three views in the app: 69 | 70 | 1. Main view: Display ERC20 token stats and send tokens 71 | 2. Private key QR code scanner: For importing user's private key 72 | 3. Receiver Public key QR code scanner: To send a token to another user, you need to scan the user's public key 73 | 74 | Learn more about the mobile frontend: [Mobile app tutorial](mobile/tutorial.md) 75 | -------------------------------------------------------------------------------- /contracts/BasicToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | import "./ERC20Basic.sol"; 5 | import "./SafeMath.sol"; 6 | 7 | 8 | /** 9 | * @title Basic token 10 | * @dev Basic version of StandardToken, with no allowances. 11 | */ 12 | contract BasicToken is ERC20Basic { 13 | using SafeMath for uint256; 14 | 15 | mapping(address => uint256) balances; 16 | 17 | uint256 totalSupply_; 18 | 19 | /** 20 | * @dev total number of tokens in existence 21 | */ 22 | function totalSupply() public view returns (uint256) { 23 | return totalSupply_; 24 | } 25 | 26 | /** 27 | * @dev transfer token for a specified address 28 | * @param _to The address to transfer to. 29 | * @param _value The amount to be transferred. 30 | */ 31 | function transfer(address _to, uint256 _value) public returns (bool) { 32 | require(_to != address(0)); 33 | require(_value <= balances[msg.sender]); 34 | 35 | // SafeMath.sub will throw if there is not enough balance. 36 | balances[msg.sender] = balances[msg.sender].sub(_value); 37 | balances[_to] = balances[_to].add(_value); 38 | Transfer(msg.sender, _to, _value); 39 | return true; 40 | } 41 | 42 | /** 43 | * @dev Gets the balance of the specified address. 44 | * @param _owner The address to query the the balance of. 45 | * @return An uint256 representing the amount owned by the passed address. 46 | */ 47 | function balanceOf(address _owner) public view returns (uint256 balance) { 48 | return balances[_owner]; 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /contracts/ERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./ERC20Basic.sol"; 4 | 5 | 6 | /** 7 | * @title ERC20 interface 8 | * @dev see https://github.com/ethereum/EIPs/issues/20 9 | */ 10 | contract ERC20 is ERC20Basic { 11 | function allowance(address owner, address spender) public view returns (uint256); 12 | function transferFrom(address from, address to, uint256 value) public returns (bool); 13 | function approve(address spender, uint256 value) public returns (bool); 14 | event Approval(address indexed owner, address indexed spender, uint256 value); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /contracts/ERC20Basic.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title ERC20Basic 6 | * @dev Simpler version of ERC20 interface 7 | * @dev see https://github.com/ethereum/EIPs/issues/179 8 | */ 9 | contract ERC20Basic { 10 | function totalSupply() public view returns (uint256); 11 | function balanceOf(address who) public view returns (uint256); 12 | function transfer(address to, uint256 value) public returns (bool); 13 | event Transfer(address indexed from, address indexed to, uint256 value); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /contracts/ExampleToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | import './StandardToken.sol'; 3 | 4 | contract ExampleToken is StandardToken { 5 | string public name = "ExampleToken"; 6 | string public symbol = "EXT"; 7 | uint public decimals = 18; 8 | uint public INITIAL_SUPPLY = 10000 * (10 ** decimals); 9 | uint256 public totalSupply; 10 | 11 | function ExampleToken() public { 12 | totalSupply = INITIAL_SUPPLY; 13 | balances[msg.sender] = INITIAL_SUPPLY; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/SafeMath.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | 4 | /** 5 | * @title SafeMath 6 | * @dev Math operations with safety checks that throw on error 7 | */ 8 | library SafeMath { 9 | 10 | /** 11 | * @dev Multiplies two numbers, throws on overflow. 12 | */ 13 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 14 | if (a == 0) { 15 | return 0; 16 | } 17 | uint256 c = a * b; 18 | assert(c / a == b); 19 | return c; 20 | } 21 | 22 | /** 23 | * @dev Integer division of two numbers, truncating the quotient. 24 | */ 25 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 26 | // assert(b > 0); // Solidity automatically throws when dividing by 0 27 | uint256 c = a / b; 28 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 29 | return c; 30 | } 31 | 32 | /** 33 | * @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). 34 | */ 35 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 36 | assert(b <= a); 37 | return a - b; 38 | } 39 | 40 | /** 41 | * @dev Adds two numbers, throws on overflow. 42 | */ 43 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 44 | uint256 c = a + b; 45 | assert(c >= a); 46 | return c; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /contracts/StandardToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.18; 2 | 3 | import "./BasicToken.sol"; 4 | import "./ERC20.sol"; 5 | 6 | 7 | /** 8 | * @title Standard ERC20 token 9 | * 10 | * @dev Implementation of the basic standard token. 11 | * @dev https://github.com/ethereum/EIPs/issues/20 12 | * @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol 13 | */ 14 | contract StandardToken is ERC20, BasicToken { 15 | 16 | mapping (address => mapping (address => uint256)) internal allowed; 17 | 18 | 19 | /** 20 | * @dev Transfer tokens from one address to another 21 | * @param _from address The address which you want to send tokens from 22 | * @param _to address The address which you want to transfer to 23 | * @param _value uint256 the amount of tokens to be transferred 24 | */ 25 | function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { 26 | require(_to != address(0)); 27 | require(_value <= balances[_from]); 28 | require(_value <= allowed[_from][msg.sender]); 29 | 30 | balances[_from] = balances[_from].sub(_value); 31 | balances[_to] = balances[_to].add(_value); 32 | allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); 33 | Transfer(_from, _to, _value); 34 | return true; 35 | } 36 | 37 | /** 38 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 39 | * 40 | * Beware that changing an allowance with this method brings the risk that someone may use both the old 41 | * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 42 | * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 43 | * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 44 | * @param _spender The address which will spend the funds. 45 | * @param _value The amount of tokens to be spent. 46 | */ 47 | function approve(address _spender, uint256 _value) public returns (bool) { 48 | allowed[msg.sender][_spender] = _value; 49 | Approval(msg.sender, _spender, _value); 50 | return true; 51 | } 52 | 53 | /** 54 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 55 | * @param _owner address The address which owns the funds. 56 | * @param _spender address The address which will spend the funds. 57 | * @return A uint256 specifying the amount of tokens still available for the spender. 58 | */ 59 | function allowance(address _owner, address _spender) public view returns (uint256) { 60 | return allowed[_owner][_spender]; 61 | } 62 | 63 | /** 64 | * @dev Increase the amount of tokens that an owner allowed to a spender. 65 | * 66 | * approve should be called when allowed[_spender] == 0. To increment 67 | * allowed value is better to use this function to avoid 2 calls (and wait until 68 | * the first transaction is mined) 69 | * From MonolithDAO Token.sol 70 | * @param _spender The address which will spend the funds. 71 | * @param _addedValue The amount of tokens to increase the allowance by. 72 | */ 73 | function increaseApproval(address _spender, uint _addedValue) public returns (bool) { 74 | allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); 75 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 76 | return true; 77 | } 78 | 79 | /** 80 | * @dev Decrease the amount of tokens that an owner allowed to a spender. 81 | * 82 | * approve should be called when allowed[_spender] == 0. To decrement 83 | * allowed value is better to use this function to avoid 2 calls (and wait until 84 | * the first transaction is mined) 85 | * From MonolithDAO Token.sol 86 | * @param _spender The address which will spend the funds. 87 | * @param _subtractedValue The amount of tokens to decrease the allowance by. 88 | */ 89 | function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) { 90 | uint oldValue = allowed[msg.sender][_spender]; 91 | if (_subtractedValue > oldValue) { 92 | allowed[msg.sender][_spender] = 0; 93 | } else { 94 | allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); 95 | } 96 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 97 | return true; 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /mobile/app/main.json: -------------------------------------------------------------------------------- 1 | { 2 | "$jason": { 3 | "head": { 4 | "title": "Main e20", 5 | "agents": { 6 | "erc20": { "url": "https://gliechtenstein.github.io/erc20/web" } 7 | }, 8 | "actions": { 9 | "fetched": { 10 | "type": "$set", 11 | "options": "{{$jason}}", 12 | "success": { 13 | "type": "$render" 14 | } 15 | }, 16 | "$show": { "trigger": "balance" }, 17 | "$foreground": { "trigger": "balance" }, 18 | "balance": [{ 19 | "{{#if 'wallet' in $global}}": { 20 | "type": "$agent.request", 21 | "options": { 22 | "id": "erc20", 23 | "method": "call", 24 | "params": [ "balanceOf", [ "{{$global.wallet.address}}" ] ] 25 | } 26 | } 27 | }], 28 | "transfer": { 29 | "type": "$href", 30 | "options": { 31 | "url": "https://gliechtenstein.github.io/erc20/mobile/qr/publickey.json", 32 | "options": { 33 | "caption": "Scan a QR code for Ethereum public key" 34 | } 35 | }, 36 | "success": { 37 | "type": "$util.alert", 38 | "options": { 39 | "title": "How many tokens?", 40 | "descriptions": "Enter the number of tokens to send", 41 | "form": [ 42 | { "name": "receiver", "value": "{{$jason.response}}" }, 43 | { "name": "number", "placeholder": "Number of tokens to send" } 44 | ] 45 | }, 46 | "success": { 47 | "type": "$agent.request", 48 | "options": { 49 | "id": "erc20", 50 | "method": "transaction", 51 | "params": [ "transfer", [ "{{$jason.receiver}}", "{{$jason.number}}" ] ] 52 | }, 53 | "success": { 54 | "type": "$href", 55 | "options": { 56 | "url": "https://gliechtenstein.github.io/erc20/mobile/wallet/wallet.json", 57 | "options": { 58 | "tx": "{{$jason.tx}}" 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "styles": { 67 | "dark": { "background": "#1a1a1a", "color": "#ffffff" } 68 | }, 69 | "templates": { 70 | "body": { 71 | "style": { "background": "#1a1a1a", "color": "#ffffff", "border": "none" }, 72 | "header": { 73 | "title": "ERC20 token app", 74 | "style": { "background": "#1a1a1a", "color": "#ffffff" } 75 | }, 76 | "layers": [ 77 | { 78 | "type": "image", 79 | "url": "https://png.icons8.com/ios-glyphs/100/ffffff/key.png", 80 | "action": { 81 | "type": "$href", 82 | "options": { 83 | "url": "https://gliechtenstein.github.io/erc20/mobile/qr/privatekey.json", 84 | "options": { 85 | "caption": "Import private key by scanning QR code" 86 | } 87 | } 88 | }, 89 | "style": { "width": "50", "height": "50", "bottom": "20", "right": "20" } 90 | } 91 | ], 92 | "sections": [ 93 | { 94 | "items": [{ 95 | "type": "label", 96 | "text": "{{$global.wallet ? 'signed in: ' + $global.wallet.address : 'not signed in'}}", 97 | "class": "dark" 98 | }] 99 | }, 100 | { 101 | "items": { 102 | "{{#each Object.keys($get)}}": { 103 | "type": "vertical", 104 | "components": [ 105 | { "type": "label", "class": "dark", "style": { "font": "HelveticaNeue-Bold", "size": "12" }, "text": "{{this}}" }, 106 | { "type": "label", "class": "dark", "text": "{{$root.$get[this].toString()}}" } 107 | ] 108 | } 109 | } 110 | }, 111 | { 112 | "items": [{ 113 | "type": "button", 114 | "text": "Transfer Token", 115 | "action": { 116 | "trigger": "transfer" 117 | }, 118 | "style": { "padding": "10", "size": "16", "background": "#7FA100", "color": "#ffffff" } 119 | }] 120 | } 121 | ] 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /mobile/app/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "$jason": { 3 | "head": { 4 | "title": "One Touch Transfer", 5 | "agents": { 6 | "erc20": { "url": "https://gliechtenstein.github.io/erc20/web" } 7 | }, 8 | "actions": { 9 | "fetched": { 10 | "type": "$set", 11 | "options": "{{$jason}}", 12 | "success": { 13 | "type": "$render" 14 | } 15 | }, 16 | "$show": { "type": "$render" }, 17 | "$foreground": { "type": "$render" }, 18 | "send": { 19 | "type": "$agent.request", 20 | "options": { 21 | "id": "erc20", 22 | "method": "transaction", 23 | "params": [ "transfer", [ "0x1ecdbe44e61249e7fB098AC07480E50D84E3E9e0", "1" ] ] 24 | }, 25 | "success": { 26 | "type": "$href", 27 | "options": { 28 | "url": "https://gliechtenstein.github.io/erc20/mobile/wallet/wallet.json", 29 | "options": { 30 | "tx": "{{$jason.tx}}" 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "styles": { 37 | "dark": { "background": "#1a1a1a", "color": "#ffffff" } 38 | }, 39 | "templates": { 40 | "body": { 41 | "style": { "background": "#1a1a1a", "color": "#ffffff", "border": "none" }, 42 | "header": { 43 | "title": "Give 1 Token to Ethan", 44 | "style": { "background": "#1a1a1a", "color": "#ffffff" } 45 | }, 46 | "layers": [ 47 | { 48 | "type": "label", 49 | "text": "Send!", 50 | "action": { 51 | "trigger": "send" 52 | }, 53 | "style": { "width": "200", "top": "50%-100", "left": "50%-100", "height": "200", "align": "center", "color": "#ffffff", "size": "40", "background": "#ff0000", "corner_radius": "100", "font": "HelveticaNeue-Bold" } 54 | }, 55 | { 56 | "type": "image", 57 | "url": "https://png.icons8.com/ios-glyphs/100/ffffff/key.png", 58 | "action": { 59 | "type": "$href", 60 | "options": { 61 | "url": "https://gliechtenstein.github.io/erc20/mobile/qr/privatekey.json", 62 | "options": { 63 | "caption": "Import private key by scanning QR code" 64 | } 65 | } 66 | }, 67 | "style": { "width": "30", "height": "30", "bottom": "20", "right": "20" } 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /mobile/qr/privatekey.json: -------------------------------------------------------------------------------- 1 | { 2 | "@": "https://gliechtenstein.github.io/erc20/mobile/qr/scanner.json", 3 | "onscan": { 4 | "type": "$set", 5 | "options": { 6 | "imported": "{{$jason.content}}" 7 | }, 8 | "success": { 9 | "type": "$util.alert", 10 | "options": { 11 | "title": "Enter Password", 12 | "description": "Enter a password to encrypt the wallet", 13 | "form": [ 14 | { 15 | "name": "password", 16 | "type": "secure" 17 | } 18 | ] 19 | }, 20 | "success": { 21 | "type": "$agent.request", 22 | "options": { 23 | "id": "eth", 24 | "method": "encrypt", 25 | "params": [ 26 | "{{$get.imported}}", 27 | "{{$jason.password}}" 28 | ] 29 | }, 30 | "success": { 31 | "type": "$global.set", 32 | "options": { 33 | "wallet": "{{$jason}}" 34 | }, 35 | "success": { 36 | "type": "$back" 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mobile/qr/publickey.json: -------------------------------------------------------------------------------- 1 | { 2 | "@": "https://gliechtenstein.github.io/erc20/mobile/qr/scanner.json", 3 | "onscan": { 4 | "type": "$ok", 5 | "options": { "response": "{{$jason.content.split(':')[1]}}" } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /mobile/qr/scanner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$jason": { 3 | "head": { 4 | "title": "QR Scanner", 5 | "agents": { 6 | "eth": { "url": "https://gliechtenstein.github.io/erc20/mobile/wallet/wallet.html" } 7 | }, 8 | "actions": { 9 | "$vision.ready": { 10 | "type": "$vision.scan" 11 | }, 12 | "$vision.onscan": { 13 | "@": "$document.onscan" 14 | }, 15 | "$load": { 16 | "type": "$render" 17 | } 18 | }, 19 | "templates": { 20 | "body": { 21 | "background": { 22 | "type": "camera", 23 | "options": { 24 | "device": "back" 25 | } 26 | }, 27 | "layers": [ 28 | { 29 | "type": "label", 30 | "text": "{{$params.caption}}", 31 | "style": { 32 | "background": "rgba(0,0,0,0.7)", 33 | "padding": "10", 34 | "color": "#ffffff", 35 | "font": "HelveticaNeue", 36 | "size": "12" 37 | } 38 | } 39 | ] 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /mobile/tutorial.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | It will be much easier to understand how everything works if you understand how Jasonette works. A quick way to catch up on Jasonette: 4 | 5 | 1. Read the tutorial at: https://medium.freecodecamp.org/how-to-build-cross-platform-mobile-apps-using-nothing-more-than-a-json-markup-f493abec1873 6 | 2. Watch the intro video on basics of Jasonette markup syntax: http://docs.jasonette.com/#d-learn-jason-syntax 7 | 8 | Also, remember the entire app is set up to run on Rinkeby testnet, so don't forget to use rinkeby address for everything. 9 | 10 | 11 | ## Step 1. Launch App 12 | 13 | 1. Download Jasonette: http://docs.jasonette.com/#quickstart 14 | 2. Set up the app with the JSON url: https://gliechtenstein.github.io/erc20/mobile/app/main.json 15 | 3. Run! 16 | 17 | You should see the main UI. 18 | 19 | ## Step 2. Import your Rinkeby private key 20 | 21 | To sign in with your rinkeby address: 22 | 23 | 1. Export your private key 24 | 2. Create a QR code from the private key: You can do it at https://www.qr-code-generator.com/ or any other qr code generator 25 | 3. From the app, press the key button at the bottom right corner. This should send you to the QR code scanner 26 | 4. Scan it and it should ask for a password. Enter the password and it will encrypt the wallet using the password, and return to the main view. 27 | 5. At this point you should see the top row has changed to "signed in", along with your public key value. 28 | 29 | ## Step 3. Send a token 30 | 31 | To send an ERC20 token to another rinkeby address, 32 | 33 | 1. Get another rinkeby address (or generate one) from metamask 34 | 2. Press the green "Transfer Token" button 35 | 3. It will take you to another QR code screen. 36 | 4. From the metamask wallet, select "Show QR Code" and it should show you the QR code. 37 | 5. Scan it and it should ask you for the password you entered earlier to encrypt the wallet. 38 | 6. Once you enter the password, it should take a bit to decrypt the wallet, sign the transaction, and then broadcast it to the network. Finally it will bring you back to the main screen. Done! 39 | 40 | ## Step 4. Try alternative UI 41 | 42 | You can try the alternative UI at https://gliechtenstein.github.io/erc20/mobile/app/simple.json 43 | 44 | 45 | # Overview 46 | 47 | Here's how the app is structured: 48 | 49 | ![structure](https://gliechtenstein.github.io/images/eth_structure.png) 50 | 51 | Above diagram is a quick overview of the overall data flow. 52 | 53 | - The user interacts with the native UI.  54 | - The native UI makes a request to the “DApp container” (which contains your web3 DApp) 55 | - And the DApp container makes a request to the “Wallet container” (for now, just think of it as a mobile equivalent of MetaMask) 56 | - The Wallet container then connects to Ethereum 57 | 58 | All of these are inter-connected through the JSON-RPC protocol, and the application — all three modules — is entirely described as a JSON markup. 59 | 60 | ![markup](https://gliechtenstein.github.io/images/jasonette_markup.png) 61 | 62 | Now let’s take a look at each module in detail. 63 | 64 | > Note: 65 | > 66 | > If this is the first time you’ve heard of Jasonette, I recommend reading this quick intro to Jasonette first: 67 | 68 | - Read tutorial: [How to build cross-platform mobile apps using nothing more than a JSON markup](https://medium.freecodecamp.org/how-to-build-cross-platform-mobile-apps-using-nothing-more-than-a-json-markup-f493abec1873) 69 | - Watch video tutorial on JASON syntax: [Building a native app in 12 minutes with Jasonette](http://docs.jasonette.com/#d-learn-jason-syntax) 70 | 71 | ## 1. Let's build a DApp container in JSON 72 | 73 | To integrate your existing DApp into the native app using Jasonette, you simply embed the DApp into the native app as a sandboxed HTML/JavaScript execution context ([“agent”](http://jasonette.com/agent)). It’s almost like embedding as an IFRAME but the difference is: 74 | 75 | 1. You’re embedding it into a mobile app, not into another web page. 76 | 2. Unlike an iframe, a 2-way communication channel is established by default 77 | 78 | Declaring an agent involves simply adding 3 lines of JSON to the existing app markup: 79 | 80 | ``` 81 | { 82 | "$jason": { 83 | "head": { 84 | "title": "Web3 DApp in a mobile app", 85 | "agents": { 86 | "eth": { 87 | "url": "https://gliechtenstein.github.io/erc20/web" 88 | } 89 | }, 90 | ... 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | We initialize the DApp container by loading from https://gliechtenstein.github.io/erc20/web and name it eth, which will be used as the ID when we make JSON-RPC calls in the future. 97 | 98 | ![agent initialize](https://gliechtenstein.github.io/images/jasonette_initialize.png) 99 | 100 | Once initialized, we can [make JSON-RPC calls into this DApp container](http://docs.jasonette.com/agents/#3-make-json-rpc-calls) and [natively handle events](http://docs.jasonette.com/actions/#action-registry) triggered from the DApp container, all by using just a JSON markup. The overall event & data flow will look like this: 101 | 102 | ![agent communication](https://gliechtenstein.github.io/images/agent_communication.png) 103 | 104 | Here’s the overview of above diagram: 105 | 106 | 1. The user ONLY interacts with the native UI. To the user the DApp is invisible and it simply functions as a data source. **(Step 1)** 107 | 2. The native module forwards the user request to the DApp container through JSON-RPC **(Step 2)** 108 | 3. The DApp container then makes a request to Ethereum network **(Step 3)**, and when it gets a response back **(Step 4)**, forwards it back to the parent native app **(Step 5)** 109 | 110 | For example, we can make a JSON-RPC call into our DApp container every time the view is displayed, by listening to a `$show` event and making an `$agent.request` action call (Step 2 from the diagram): 111 | 112 | 113 | ``` 114 | { 115 | "$jason": { 116 | "head": { 117 | ... 118 | "actions": { 119 | "$show": [{ 120 | "{{#if 'wallet' in $global}}": { 121 | "type": "$agent.request", 122 | "options": { 123 | "id": "eth", 124 | "method": "call", 125 | "params": [ "balanceOf", [ "{{$global.wallet.address}}" ] ] 126 | }, 127 | "success": { 128 | ... 129 | } 130 | } 131 | }], 132 | ... 133 | ``` 134 | 135 | Above markup will first check if a global variable named wallet exists (`"{{#if ‘wallet' in $global}}"`). If it does, it means the user has already imported a wallet previously so we go on to the next step, the `"$agent.request"` action. 136 | 137 | > Quick Primer on Jasonette Actions:  138 | > 139 | > Actions are like functions, expressed in JSON. All [“actions”](https://docs.jasonette.com/actions/) in Jasonette are described with at most four attributes: [`"type"`, `"options"`, `"success"`, and `"error"`](https://docs.jasonette.com/actions/#syntax). 140 | > 141 | > `"type"` is the name of the action (sort of like an API method), `"options"` is the parameters you pass to it, `"success"` is the callback that gets executed when the action succeeds. `"error"` is the error callback that gets run when something goes wrong. The `"success"` and `"error"` callbacks can be nested to create a sequence of instructions. 142 | 143 | 144 | The above JSON markup describes “Find an agent named eth (which we initialized from above), then find a JavaScript function named call from the agent. And finally, run it by passing the arguments `"balanceOf"` and `["{{$global.wallet.address}}"]` which evaluates to the wallet address if the user has already imported one. 145 | So if the wallet address was `"0xabcdefghijklmnop"`, above JSON markup will make a JavaScript function call: 146 | 147 | ``` 148 | call("balanceOf", ["0xabcdefghijklmnop"]) 149 | ``` 150 | 151 | If we look into the agent to see what’s going on inside the call JavaScript function, it would look something like this: 152 | 153 | ``` 154 | var call = function( ... ) { 155 | ... 156 | contract.balanceOf.call(function(err, result) { 157 | ... 158 | }) 159 | } 160 | ``` 161 | 162 | The DApp requests a [balanceOf method call](https://theethereum.wiki/w/index.php/ERC20_Token_Standard#Token_Balance) to the deployed ERC20 token contract and waits for the response with a callback function. (**Step 3** and **step 4** from the diagram) 163 | 164 | When we get the response back from Ethereum network, we need to send it back to the native app who requested it in the first place through $agent.request API. We do this through the built-in [$agent.response](http://docs.jasonette.com/agents/#2-agentresponse) method **(Step 5)** which gets injected to every agent on load. 165 | 166 | ``` 167 | contract.balanceOf.call(function(err, result) { 168 | ... 169 | $agent.response(result); 170 | }) 171 | ``` 172 | 173 | Note that we’re basically using our DApp as a “backend” to the mobile app instead of rendering to its own DOM. The `$agent.response` JavaScript function will return the result back to the $agent.request action which triggered off everything. We’ll take a look at how we deal with this in the next section. 174 | 175 | ## 2. Let’s Build the Native UI in JSON 176 | 177 | Now that we have the data from the `$agent.response` method call from the previous section, we are all ready to render it. Let’s return back to the original `$agent.request` action. Previously we only looked at the options object, but this time we will focus on the `"success"` callback which gets triggered when the agent responds through the `$agent.response` JavaScript method. 178 | 179 | ``` 180 | { 181 | "$jason": { 182 | "head": { 183 | ... 184 | "actions": { 185 | "$show": [{ 186 | "{{#if 'wallet' in $global}}": { 187 | "type": "$agent.request", 188 | "options": { 189 | "id": "eth", 190 | "method": "call", 191 | "params": [ "balanceOf", [ "{{$global.wallet.address}}" ] ] 192 | }, 193 | "success": { 194 | "type": "$set", 195 | "options": "{{$jason}}", 196 | "success": { 197 | "type": "$render" 198 | } 199 | } 200 | } 201 | }], 202 | ... 203 | ``` 204 | 205 | In the `"success”` callback we see another action ["$set"](http://docs.jasonette.com/actions/#set). This action is used to set a local variable. 206 | 207 | We also see a template expression `"{{$jason}}"`. That expression would evaluate to the value returned from the agent via `$agent.response`. 208 | 209 | > Quick Intro: How do return values work? 210 | > 211 | > In Jasonette whenever an action is run, its return value is always returned as $jason inside the next `"success"` callback action. 212 | 213 | In this case if the return value was `{ "balanceOf": 100000 }`, that will be the value of `$jason` when the `$set` action is executed. The template expression would evaluate to the following result: 214 | 215 | ``` 216 | ... 217 | "type": "$set", 218 | "options": { 219 | "balanceOf": 100000 220 | }, 221 | "success": { 222 | "type": "$render" 223 | } 224 | ... 225 | ``` 226 | 227 | The $set action will set the local variable `"balanceOf"`’s value to `100000`. 228 | 229 | After the local variable is set, it goes onto the next success callback action ["$render"](http://docs.jasonette.com/actions/#render). The `$render` action renders the template with the current data on the stack. The `$render` action automatically looks for a JSON template under `$jason.head.templates.body` and renders it if it finds one. 230 | 231 | The rendering is a two step process: 232 | 233 | 1. **Parse:** Select the data JSON in the current stack and transform it with a template JSON 234 | 2. **Render:** Interpret the transformed result into native UI layout. 235 | 236 | The first step is “parsing” and it’s done by a library called [SelectTransform](https://selecttransform.github.io/site/) — built into Jasonette as a dependency—which is kind of like [Handlerbars](https://handlebarsjs.com/), but instead of using an HTML template, it uses a JSON template to transform one JSON to another: 237 | 238 | ![ST](https://selecttransform.github.io/site/src/st.gif) 239 | 240 | After the parsing is complete, the second step is to actually render the parsed markup into a native layout. The Jasonette rendering engine takes the parsed result from step 1 and interprets it and turns it into native layout according to the syntax you can find here: 241 | 242 | - [Jasonett View Markup Syntax](http://docs.jasonette.com/document/#body) 243 | 244 | The native layout engine is flexible enough to describe most of what you would want in a native app, purely in a single JSON markup. Here are some places where you can check out public examples: 245 | 246 | - [Jasonette Example Gallery](http://docs.jasonette.com/examples/) 247 | - [Jasonette Web](http://web.jasonette.com/) 248 | 249 | For our MVP app, we will serve it from a remote JSON hosted on github. The template starts here: https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L70 250 | 251 | The end result would look something like this: 252 | 253 | ![erc app](https://gliechtenstein.github.io/images/jasonette_erc.png) 254 | 255 | ## 3. Let’s Build a QR Code Scanner in JSON 256 | 257 | From the app you will notice the key icon at the bottom right corner. When you press it, it should take you to a full screen camera screen that looks like this: 258 | 259 | ![jasonette qr code scanner](https://gliechtenstein.github.io/images/jasonette_qr.jpeg) 260 | 261 | That’s because there’s an [`$href` action](https://docs.jasonette.com/actions/#href) attached to that button, which sends you to the URL that contains another JSON markup that self-constructs into the QR code scanner view: 262 | 263 | ``` 264 | ... 265 | { 266 | "type": "image", 267 | "url": "https://png.icons8.com/ios-glyphs/100/ffffff/key.png", 268 | "action": { 269 | "type": "$href", 270 | "options": { 271 | "url": "https://gliechtenstein.github.io/erc20/mobile/qr/privatekey.json", 272 | "options": { 273 | "caption": "Import private key by scanning QR code" 274 | } 275 | } 276 | } 277 | }, 278 | ... 279 | ``` 280 | 281 | This QR code scanner view is also described entirely as a JSON markup. If you look at the link https://gliechtenstein.github.io/erc20/mobile/qr/privatekey.json you will see that it starts with: 282 | 283 | ``` 284 | { 285 | "@": "https://gliechtenstein.github.io/erc20/mobile/qr/scanner.json", 286 | "onscan": { 287 | ... 288 | ``` 289 | 290 | The `"@"` attribute is called [Mixin](https://docs.jasonette.com/mixin/), and it lets you mix in another JSON into the current JSON at load time. Basically when this JSON document is loaded, it immediately makes another request to fetch the JSON at https://gliechtenstein.github.io/erc20/mobile/qr/scanner.json and mixes all its root attributes into the current JSON tree. You can learn more about mixins here: 291 | 292 | - [Mixin Documentation](https://docs.jasonette.com/mixin/) 293 | - Mixin Tutorial Series 294 | - Remote mixin: http://blog.jasonette.com/2017/02/27/mixins/ 295 | - Self mixin: http://blog.jasonette.com/2017/03/02/self-mixin/ 296 | 297 | After all the mixin has been resolved we are left with a JSON that mostly looks like the [scanner.json](scanner.json) but with the `"onscan"` attribute mixed into `$jason.head.actions.$vision.onscan`. 298 | 299 | - The background is set to `"type": "camera"`  300 | - The “import private key by scanning QR code” caption is a floating label layer 301 | 302 | And we also listen for QR code scan event using the `$vision.onscan` event. Here’s what happens when the QR code is scanned: 303 | 304 | 1. The `$vision.onscan` event is triggered 305 | 2. It asks for a user input password using `$util.alert`. 306 | 3. It calls an agent function to encrypt the scanned private key with the password. 307 | 4. Take the encrypted wallet and set it as `$global.wallet` variable 308 | 5. Return back to the main view, thanks to [$back](https://docs.jasonette.com/actions/#back) action. 309 | 310 | You can learn more about QR code scanning here: 311 | 312 | - Tutorial: Build a QRCode/Barcode scanning app with 26 lines of JSON: https://medium.com/@gliechtenstein/build-a-qrcode-barcode-scanning-app-with-26-lines-of-json-b83453d39197 313 | - Documentation: http://docs.jasonette.com/actions/#vision 314 | 315 | ## 4. Let’s Build a Wallet Container in JSON 316 | 317 | We have so far learned how to query Ethereum, how to import a private key using QR code. Now it’s time to actually spend money from the wallet we imported. 318 | 319 | Writing to Ethereum is trickier than reading. We must be more careful because it deals with creating actual transactions and sending real money. 320 | 321 | Normally when building a regular DApp, we use the web3.js library to make an API call like this: 322 | 323 | ``` 324 | contract.transfer.sendTransaction(receiver, tokens, { 325 | to: contract.address, 326 | gasLimit: 21000, 327 | gasPrice: 20000000000 328 | }, function(err, result) { 329 | // Render the DOM with result 330 | }) 331 | ``` 332 | 333 | This method sendTransaction actually does two things: 334 | 335 | 1. Create an encoded transaction object for a contract method called "transfer". 336 | 2. Sign and broadcast the transaction object to Ethereum via JSON-RPC. 337 | 338 | For our project, instead of having the DApp handle both, we will: 339 | 340 | 1. Let our DApp container handle only the first part of encoding a transaction 341 | 2. and create a new separate container called “Wallet container” to handle the second part 342 | 343 | By separating the two, the DApp container doesn’t have to deal with private keys. Instead it delegates it to the wallet container just like how MetaMask deals with this issue automatically. That way the DApp developer can just focus on the application logic. 344 | 345 | So instead of using the sendTransaction method, we first use an API called getData to get a transaction object: 346 | 347 | ``` 348 | var tx = contract.transfer.getData(receiver, tokens) 349 | ``` 350 | 351 | And then pass that back to the parent app through the $agent.response API: 352 | 353 | ``` 354 | $agent.response({ tx: tx }) 355 | ``` 356 | 357 | The parent native app then will pass it along to our new wallet view. 358 | 359 | ![wallet](https://gliechtenstein.github.io/images/jasonette_wallet.png) 360 | 361 | The wallet view (and the wallet agent it contains) will take this unsigned transaction data, sign it, and then broadcast it through [Infura](https://infura.io). You can check out the wallet view source code here: https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json 362 | 363 | Here’s the wallet agent code: https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.html 364 | 365 | Note that the “wallet view” is a completely separate sandboxed view of its own, just like how [MetaMask](https://metamask.io/) opens up in a new popup browser. This is by design. This insulates the DApp developer from ever having to deal with private keys. 366 | 367 | 368 | 369 | # Step by Step Explanation on Implementation Details 370 | 371 | We've looked at how the app is structured on a high level. Now let's walk through each file to see how it's **actually** implemented. 372 | 373 | First, it's easier to understand everything if you remember that each JSON file is a view (or gets mixed into another JSON to form a view). It's pretty much like HTML in that everything centers around the concept of a "page", except that every "page" is a native view. 374 | 375 | There are three folders: 376 | 377 | - **app:** contains the main app view JSON markup 378 | - **qr:** QR code scanner markup 379 | - **wallet:** JSON markup for handling private keys, signing, and broadcasting to rinkeby network. 380 | 381 | Let's go through one by one. 382 | 383 | ## /app 384 | 385 | ### main.json 386 | 387 | ![erc20](https://gliechtenstein.github.io/images/erc20.gif) 388 | 389 | This is the main user interface. 390 | 391 | First of all, it's important to notice that we're reusing the web app in the mobile app. You can see the web app at [https://gliechtenstein.github.io/erc20/web](https://gliechtenstein.github.io/erc20/web) is being loaded as an agent: 392 | 393 | ``` 394 | { 395 | "$jason": { 396 | "head": { 397 | "title": "Main e20", 398 | "agents": { 399 | "erc20": { "url": "https://gliechtenstein.github.io/erc20/web" } 400 | }, 401 | ... 402 | ``` 403 | 404 | You can view the code for the web app under [web/index.html](web/index.html). 405 | 406 | In this case we're using the same web app as an agent for simplicity, but you don't have to. You can write a minimal HTML specifically for agent usage. 407 | 408 | Also we're loading the agent from the remote https URL, but you can also [package the HTML file locally and load it over `file://` url scheme](http://docs.jasonette.com/offline/#1-loading-from-local-file). 409 | 410 | 411 | 412 | Now let's go through the code. 413 | 414 | 415 | When the app loads (`$show` or `$foreground`) it triggers a `balance` action. 416 | 417 | The `balance` action looks like this: 418 | 419 | ``` 420 | "balance": [{ 421 | "{{#if 'wallet' in $global}}": { 422 | "type": "$agent.request", 423 | "options": { 424 | "id": "erc20", 425 | "method": "call", 426 | "params": [ "balanceOf", [ "{{$global.wallet.address}}" ] ] 427 | } 428 | } 429 | }], 430 | ``` 431 | 432 | It looks for a global variable named `wallet`. If such a variable exists, it means we've imported a wallet already (This app is just an MVP so it only supports one private key under `$global.wallet`) 433 | 434 | Then it makes an `$agent.request` into an agent named `erc20`, which is defined under: `$jason.head.agents.erc20`, which links to [https://gliechtenstein.github.io/erc20/web](https://gliechtenstein.github.io/erc20/web). 435 | 436 | The `options` object passed to the `$agent.request` action is the actual JSON-RPC request. 437 | 438 | This request calls a function named `call`, and passes `"balanceOf"` and the wallet's address (public key) if there is one. 439 | 440 | Then you look into the agent DApp's `call` function. 441 | 442 | If you look inside the `call` function, it basically makes a request to Ethereum via infura node. And when it successfully fetches the content, it triggers a `fetched` event with the response as a payload https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L104 443 | 444 | ["fetched"](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L9) is a user defined custom event. And in this case it sets the local variables with the event payload from the agent, and then goes on to the next success callback action which is [$render](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L13). 445 | 446 | The $render action renders the template under [$jason.head.templates.body](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L70) with all the data in the current execution stack, which includes the local variable we've just set. 447 | 448 | Just to highlight some of the template details: 449 | 450 | 1. It renders a wallet address if signed in (`$global.wallet` exists) https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L96 but displays "not signed in" if the variable doesn't exist yet. 451 | 2. It loops through the local variable attributes we just set from querying Ethereum via `call()` function https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L102 and displays a vertical layout of two label components https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L102 452 | 3. Also it creates a button component that says "transfer", and attaches an action handler which will trigger an action called "transfer" when tapped: https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L115 453 | 454 | You can find the user-defined `"transfer"` event under `$jason.head.actions.transfer` https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L28 455 | 456 | 457 | Let's look into what happens when the `"transfer"` event is triggered. 458 | 459 | The first thing is an `$href` to another view: https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L28 460 | 461 | It will send us to [publickey.json](#publickey.json) which will let the user scan a public key QR code, and send it back to the current view once done. 462 | 463 | With the return value from the QR code scanner view, the action call chain continues on. It goes to the next `success` callback, which is [$util.alert](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L28). It asks for how many tokes you want to send. 464 | 465 | When you answer, it will try to find an agent with and ID of [eth](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L49) and try to make a function call into it (the [transaction](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L50) function) 466 | 467 | If you look at the JavaScript code, the [transaction](https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L30) function takes a method name and calls it using the web3.js library. Since we're passing the parameters "transfer" and the user input token count from above step, the agent will try to make a "transfer" method call into the contract we instantiated from the web3.js library. 468 | 469 | The DApp agent [will look for `window.$agent`](https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L54), and since this was loaded as a Jasonette agent, it will go to execute `getData` to export the transaction and send it back to the native app: https://github.com/gliechtenstein/erc20/blob/master/web/index.html#L58 470 | 471 | Now let's not forget that we haven't even yet touched the user's private key or signed anything, not to mention broadcasting to Ethereum. We only have an unsigned raw transaction at this point. 472 | 473 | So we call the next action which opens a new view ([wallet.json](https://github.com/gliechtenstein/erc20/blob/master/mobile/app/main.json#L54)). This wallet agent is where all the private key handling will happen. It's intentionally split into these two steps (`getData` to get a raw transaction, and then send it to the wallet agent for actual signing and broadcasting) because we want to reuse this in the future. 474 | 475 | Now we head over to [wallet.json](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json), the first thing that happens is the `$load` event https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json#L9 and in this case it asks the user to enter the password used to encrypt the wallet (if the user already has imported a wallet, but if not, we will discuss how the app also has a QR code scanner for importing private keys below). 476 | 477 | After taking the password, it passes the unsigned transaction along with the password into the [wallet agent](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.html), and calls the function [send](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json#L20). 478 | 479 | Now, if we look into the wallet.html agent, the [send](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.html#L60) function basically decrypts the wallet with the password, signs the transaction and broadcasts it to Ethereum (Infura). 480 | 481 | After that it again calls [$agent.response](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.html#L84) which returns control back to the parent app, and the app goes on to display the "success!" banner https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json#L29 and goes back to the previous view via [$back](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.json#L34). 482 | 483 | And that's about it for how the app displays Ethereum data, and how you can send tokens. 484 | 485 | 486 | ### simple.json 487 | 488 | ![erc20ethan](https://gliechtenstein.github.io/images/erc20ethan.gif) 489 | 490 | This is just an alternative interface, used to demonstrate how easy it is to fork an app and build a custom one. 491 | 492 | Simply load this JSON instead of the `main.json` from above, and you'll get the default interface. 493 | 494 | I made this by tweaking the `main.json` a bit. Here are the two customizations I made: 495 | 496 | - The UI is just a simple red button that says "Send". 497 | - Instead of scanning a receiver's public key through QR code, it has a receiver address hardcoded (mine), to demonstrate transactions can be easily customized as well. 498 | 499 | https://github.com/gliechtenstein/erc20/blob/master/mobile/app/simple.json 500 | 501 | --- 502 | 503 | ## /qr 504 | 505 | This folder contains all the modules for scanning QR codes. 506 | 507 | There are three files: 508 | 509 | - [scanner.json](qr/scanner.json): This is the core QR code module which we will use as a stub. This file won't be used directly but will be mixed into the following two files for custom QR code scanning (See [mixins](http://docs.jasonette.com/mixin/) to learn how this works). This file gets mixed in to the following two files [here](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/publickey.json#L2) and [here](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/privatekey.json#L2). 510 | - [privatekey.json](qr/privatekey.json): This is a sandboxed view that scans a private key as QR code and adds to a global variable named [wallet](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/privatekey.json#L33), after which it will call the [$back action](http://docs.jasonette.com/actions/#back) to [return back to the previous view](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/privatekey.json#L36) which called this module 511 | - [publickey.json](qr/publickey.json): This is another sandboxed view inherited from [scanner.json](qr/scanner.json), but in this case it's not for importing the user's private key. Instead it's used to scan soemone else's public key for sending. It calls an [$ok](http://docs.jasonette.com/actions/#ok) action which returns the control back to the previous view but also [with a return value](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/publickey.json#L4) 512 | 513 | ### scanner.json 514 | 515 | This is the core QR scanner module, but is not meant to be used as a standalone. It's meant to be used as a mixin. Think of it as sort of an "abstract class". 516 | 517 | It's designed so that it requires a self mixin. 518 | 519 | If you look under https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/scanner.json#L13 you'll see that there's a self-mixin. The `$document.onscan` attribute means, when any JSON that uses the `scanner.json` as a mixin loads, it will look for the root level `onscan` attribute (the `$document` means the root level of the entire JSON tree) and automatically replace the `"@": "$document.onscan"` with the found subtree. I'll explain this further in the following sections where both `privatekey.json` and `publickey.json` employs this appraoch to "inherit" from this JSON file. 520 | 521 | Also note that there's a template (`$jason.head.templates.body`) which gets [rendred automatically at view load event](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/scanner.json#L16) 522 | 523 | The rendered body would include a `camera` type background which displays a full screen camera as background. And the camera will trigger `$vision.ready` event once initialized. 524 | 525 | ### privatekey.json 526 | 527 | The [onscan](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/privatekey.json#L3) attribute gets mixed in under [$vision.onscan](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/scanner.json#L13) through [self-mixin](http://docs.jasonette.com/mixin/#self-mixin). 528 | 529 | After it gets mixed in, the result would look something like this: 530 | 531 | ``` 532 | ... 533 | "actions": { 534 | "$vision.ready": { 535 | "type": "$vision.scan" 536 | }, 537 | "$vision.onscan": { 538 | "type": "$set", 539 | "options": { 540 | "imported": "{{$jason.content}}" 541 | }, 542 | "success": { 543 | "type": "$util.alert", 544 | "options": { 545 | "title": "Enter Password", 546 | "description": "Enter a password to encrypt the wallet", 547 | "form": [ 548 | { 549 | "name": "password", 550 | "type": "secure" 551 | } 552 | ] 553 | }, 554 | "success": { 555 | "type": "$agent.request", 556 | "options": { 557 | "id": "eth", 558 | "method": "encrypt", 559 | "params": [ 560 | "{{$get.imported}}", 561 | "{{$jason.password}}" 562 | ] 563 | }, 564 | "success": { 565 | "type": "$global.set", 566 | "options": { 567 | "wallet": "{{$jason}}" 568 | }, 569 | "success": { 570 | "type": "$back" 571 | } 572 | } 573 | } 574 | } 575 | }, 576 | "$load": { 577 | "type": "$render" 578 | } 579 | ... 580 | ``` 581 | 582 | 583 | 1. First, the `$vision.ready` event gets triggered, and the app triggers `$vision.scan`. 584 | 2. The camera starts scanning for barcodes. And when it detects one, it will trigger `$vision.onscan`, whose final markup looks like what we saw above. 585 | 3. When we look under `$vision.scan`, it sets a local variable named `imported` through the `$set` action. We need this local variable further down along the way. 586 | 4. Next, its `success` callback is executed. The next action is `$util.alert`. It opens an alert dialog to ask user input. 587 | 5. The user enters a password and presses OK, and the `success` callback is triggered. 588 | 6. The next action that's called as the success callback is the `$agent.request`. It makes a JSON-RPC call into the `eth` agent we described [here](https://github.com/gliechtenstein/erc20/blob/master/mobile/qr/scanner.json#L6), which initializes with a web app we wrote [here](https://github.com/gliechtenstein/erc20/blob/master/mobile/wallet/wallet.html) 589 | 7. The wallet.html agent calls the `encrypt` function with the two parameters passed in. 590 | 8. The `encrypt` function takes the private key and the password, encrypts it, and returns the result through `$agent.response` api. 591 | 9. The `$agent.response()` from the JS side then calls the next `success` callback where we left off when we called the `$agent.request`. The next action is `$global.set`. 592 | 10. Here we set an app wide global variable named `wallet` with the encrypted content we received from the agent from the previous step. 593 | 11. Finally, all jobs are done so we call `$back`, which takes the view back to the previous view which called this view. 594 | 595 | 596 | ### publickey.json 597 | 598 | This file is much simpler than `privatekey.json`. This represents a view which scans a public key QR code, and sends it back to the previous view as a return value. 599 | 600 | We use the `$ok` action to go back one step with a return value. 601 | 602 | --- 603 | 604 | ## /wallet 605 | 606 | This folder contains `wallet.json` for native UI, and `wallet.html` for an agent specifically designed to deal with user private key, signing, broadcasting, etc. 607 | 608 | Most of what they do have been explained above while explaining the `/app` folder. 609 | 610 | -------------------------------------------------------------------------------- /mobile/wallet/wallet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /mobile/wallet/wallet.json: -------------------------------------------------------------------------------- 1 | { 2 | "$jason": { 3 | "head": { 4 | "title": "Eth Transaction Sandbox", 5 | "agents": { 6 | "eth": { "url": "https://gliechtenstein.github.io/erc20/mobile/wallet/wallet.html" } 7 | }, 8 | "actions": { 9 | "$load": { 10 | "type": "$util.alert", 11 | "options": { 12 | "title": "Enter Password", 13 | "description": "Enter the password you used to encrypt the wallet", 14 | "form": [ { "name": "password", "type": "secure" } ] 15 | }, 16 | "success": { 17 | "type": "$agent.request", 18 | "options": { 19 | "id": "eth", 20 | "method": "send", 21 | "params": [ "{{$params.tx}}", "{{$global.wallet.encrypted}}", "{{$jason.password}}" ] 22 | }, 23 | "success": { 24 | "type": "$global.set", 25 | "options": { 26 | "transactions": "{{$global.transactions ? $global.transactions.concat($jason) : [$jason]}}" 27 | }, 28 | "success": { 29 | "type": "$util.banner", 30 | "options": { 31 | "title": "Transaction Sent!", "description": "Successfully sent the transaction." 32 | }, 33 | "success": { 34 | "type": "$back" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | }, 42 | "body": { 43 | "background": "#1a1a1a", 44 | "layers": [{ 45 | "type": "image", 46 | "url": "https://gliechtenstein.github.io/images/coin.gif", 47 | "style": { 48 | "top": "50%-75", 49 | "left": "50%-150", 50 | "width": "300" 51 | } 52 | }], 53 | "header": { 54 | "title": "Transaction", 55 | "style": { "background": "#1a1a1a", "color": "#ffffff" } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 244 | 245 | -------------------------------------------------------------------------------- /web/tutorial.md: -------------------------------------------------------------------------------- 1 | # Web3 DApp Tutorial 2 | 3 | ![img](https://gliechtenstein.github.io/images/dapp.png) 4 | 5 | This web app is just a single HTML file that renders into a single page app. 6 | 7 | No NPM installs, no webpacks, no babel plugins. I'm just using a dependency free library called [cell.js](https://www.celljs.org) to make it as simple as possible. That said you can build them whichever way you want. It's just a simple web app that talks to an ERC20 token I deployed to Ethereum, through [Infura](https://infura.io/) 8 | 9 | If you've ever built a web3 DApp before, everything will be probably familiar. 10 | 11 | There's only one thing that's different here from your usual DApp code, and that's how the code handles things based on `$window.agent` value. 12 | 13 | ## $window.agent 14 | 15 | When a web app is loaded as an agent, Jasonette automatically injects a global object named '$agent' which you can use to communicate with the parent native app by calling methods like: 16 | 17 | - $agent.request: Call another agent 18 | - $agent.response: Respond to a request from the parent app or another agent 19 | - $agent.trigger: Trigger a native even back to the parent app along with a payload 20 | 21 | Therefore there's a part in the code where it checks for `window.$agent`. 22 | 23 | Normally to send a transaction to a contract, your code would look like: 24 | 25 | ``` 26 | contract[method].sendTransaction.apply(this, args.concat([ 27 | tx, 28 | function(err, response) { 29 | // render the DOM with response 30 | } 31 | ])); 32 | ``` 33 | 34 | However, in our case we don't want this container to send the transaction because this container will never have access to the user's private key, so it's more secure, and the DApp developer can delegate the signing and broadcasting to another agent. 35 | 36 | That's why we have a part where it says: 37 | 38 | ``` 39 | tx.data = contract[method].getData.apply(this, args) 40 | $agent.response({ tx: tx }); 41 | ``` 42 | 43 | The `getData` method creates a transaction obejct as an exportable format instead of directly broadcasting to the network. Even if you try to broadcast at this point it wouldn't work since this container won't have access to the user's private key. 44 | 45 | After the transaction is created, we send it back to the parent app's request through `$agent.response`. 46 | 47 | The parent app will then: 48 | 49 | 1. take the return value and assign it to a variable named `$jason` 50 | 2. and go on to the next step, which is to send the transaction to the `wallet` agent. 51 | 3. And it is this `wallet` agent that actually takes care of signing and broadcasting the transaction. This wallet agent is reusable since it's loosely coupled with other containers through JSON-RPC protocol. 52 | --------------------------------------------------------------------------------