├── .gitignore ├── README.md ├── app ├── index.html ├── javascripts │ ├── app.js │ ├── hooked-web3-provider.min.js │ └── lightwallet.min.js └── stylesheets │ └── app.css ├── contracts ├── Conference.sol └── Migrations.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── test-genesis.json ├── test └── conference.js └── truffle.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | config/development/ 3 | config/test/ 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Conference 2 | 3 | A simple Ethereum smart contract and lightwallet example. 4 | 5 | For noobs! There might be bugs here. 6 | 7 | ### Updates 8 | 9 | Current code uses *Truffle v2.0.4* 10 | 11 | 12 | ### Install 13 | 14 | Install [testrpc] (or use geth) 15 | 16 | ``` 17 | $ npm install -g ethereumjs-testrpc 18 | ``` 19 | 20 | Install [truffle](https://github.com/consensys/truffle): 21 | 22 | ``` 23 | $ npm install -g truffle 24 | ``` 25 | 26 | If you don't have solc you can get it [here](https://github.com/ethereum/go-ethereum/wiki/Contract-Tutorial#using-an-online-compiler) 27 | 28 | ### Run 29 | 30 | Run testrpc in one console window: 31 | 32 | ``` 33 | $ testrpc 34 | ``` 35 | In another console window run truffle from project root directory: 36 | 37 | ``` 38 | $ truffle compile 39 | $ truffle migrate 40 | $ truffle test 41 | $ truffle serve // server at localhost:8080 42 | ``` 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Conference DApp 5 | 6 | 24 | 25 | 26 | 27 | 28 |

Conference DApp

29 |
30 | Contract deployed at:
31 |
32 |
33 | Organizer: 34 |
35 |
36 | Quota: 37 | 38 | 39 |
40 |
41 | Registrants: 0 42 |
43 | 44 |
45 | 46 |
47 |

Buy a Ticket

48 | Ticket Price: 49 | Buyer Address: 50 | 51 | 52 |
53 | 54 |
55 | 56 |
57 |

Refund a Ticket

58 | Ticket Price: 59 | Buyer Address: 60 | 61 | 62 |
63 | 64 |
65 | 66 |
67 |

Create a Wallet

68 | Password: 69 | 70 | 71 | 72 |

73 | Your Wallet Secret Seed: 74 |

75 | Your New Wallet Address: 76 |

77 | Your New Wallet Private Key: 78 |

79 | Wallet Balance: 80 |

81 | 82 |
83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/javascripts/app.js: -------------------------------------------------------------------------------- 1 | var accounts, account; 2 | var myConferenceInstance; 3 | 4 | // Initialize 5 | function initializeConference() { 6 | Conference.new({from: accounts[0], gas: 3141592}).then( 7 | function(conf) { 8 | console.log(conf); 9 | myConferenceInstance = conf; 10 | $("#confAddress").html(myConferenceInstance.address); 11 | checkValues(); 12 | }); 13 | } 14 | 15 | // Check Values 16 | function checkValues() { 17 | myConferenceInstance.quota.call().then( 18 | function(quota) { 19 | $("input#confQuota").val(quota); 20 | return myConferenceInstance.organizer.call(); 21 | }).then( 22 | function(organizer) { 23 | $("input#confOrganizer").val(organizer); 24 | return myConferenceInstance.numRegistrants.call(); 25 | }).then( 26 | function(num) { 27 | $("#numRegistrants").html(num.toNumber()); 28 | return myConferenceInstance.organizer.call(); 29 | }); 30 | } 31 | 32 | // Change Quota 33 | function changeQuota(val) { 34 | myConferenceInstance.changeQuota(val, {from: accounts[0]}).then( 35 | function() { 36 | return myConferenceInstance.quota.call(); 37 | }).then( 38 | function(quota) { 39 | if (quota == val) { 40 | var msgResult; 41 | msgResult = "Change successful"; 42 | } else { 43 | msgResult = "Change failed"; 44 | } 45 | $("#changeQuotaResult").html(msgResult); 46 | }); 47 | } 48 | 49 | // buyTicket 50 | function buyTicket(buyerAddress, ticketPrice) { 51 | 52 | myConferenceInstance.buyTicket({ from: buyerAddress, value: ticketPrice }).then( 53 | function() { 54 | return myConferenceInstance.numRegistrants.call(); 55 | }).then( 56 | function(num) { 57 | $("#numRegistrants").html(num.toNumber()); 58 | return myConferenceInstance.registrantsPaid.call(buyerAddress); 59 | }).then( 60 | function(valuePaid) { 61 | var msgResult; 62 | if (valuePaid.toNumber() == ticketPrice) { 63 | msgResult = "Purchase successful"; 64 | } else { 65 | msgResult = "Purchase failed"; 66 | } 67 | $("#buyTicketResult").html(msgResult); 68 | }); 69 | } 70 | 71 | // refundTicket 72 | function refundTicket(buyerAddress, ticketPrice) { 73 | 74 | var msgResult; 75 | 76 | myConferenceInstance.registrantsPaid.call(buyerAddress).then( 77 | function(result) { 78 | if (result.toNumber() == 0) { 79 | $("#refundTicketResult").html("Buyer is not registered - no refund!"); 80 | } else { 81 | myConferenceInstance.refundTicket(buyerAddress, 82 | ticketPrice, {from: accounts[0]}).then( 83 | function() { 84 | return myConferenceInstance.numRegistrants.call(); 85 | }).then( 86 | function(num) { 87 | $("#numRegistrants").html(num.toNumber()); 88 | return myConferenceInstance.registrantsPaid.call(buyerAddress); 89 | }).then( 90 | function(valuePaid) { 91 | if (valuePaid.toNumber() == 0) { 92 | msgResult = "Refund successful"; 93 | } else { 94 | msgResult = "Refund failed"; 95 | } 96 | $("#refundTicketResult").html(msgResult); 97 | }); 98 | } 99 | }); 100 | } 101 | 102 | // createWallet 103 | function createWallet(password) { 104 | 105 | var msgResult; 106 | 107 | var secretSeed = lightwallet.keystore.generateRandomSeed(); 108 | 109 | $("#seed").html(secretSeed); 110 | 111 | lightwallet.keystore.deriveKeyFromPassword(password, function (err, pwDerivedKey) { 112 | 113 | console.log("createWallet"); 114 | 115 | var keystore = new lightwallet.keystore(secretSeed, pwDerivedKey); 116 | 117 | // generate one new address/private key pairs 118 | // the corresponding private keys are also encrypted 119 | keystore.generateNewAddress(pwDerivedKey); 120 | 121 | var address = keystore.getAddresses()[0]; 122 | 123 | var privateKey = keystore.exportPrivateKey(address, pwDerivedKey); 124 | 125 | console.log(address); 126 | 127 | $("#wallet").html("0x"+address); 128 | $("#privateKey").html(privateKey); 129 | $("#balance").html(getBalance(address)); 130 | 131 | 132 | // Now set ks as transaction_signer in the hooked web3 provider 133 | // and you can start using web3 using the keys/addresses in ks! 134 | 135 | switchToHooked3(keystore); 136 | 137 | }); 138 | } 139 | 140 | function getBalance(address) { 141 | return web3.fromWei(web3.eth.getBalance(address).toNumber(), 'ether'); 142 | } 143 | 144 | // switch to hooked3webprovider which allows for external Tx signing 145 | // (rather than signing from a wallet in the Ethereum client) 146 | function switchToHooked3(_keystore) { 147 | 148 | console.log("switchToHooked3"); 149 | 150 | var web3Provider = new HookedWeb3Provider({ 151 | host: "http://localhost:8545", // check what using in truffle.js 152 | transaction_signer: _keystore 153 | }); 154 | 155 | web3.setProvider(web3Provider); 156 | } 157 | 158 | function fundEth(newAddress, amt) { 159 | 160 | console.log("fundEth"); 161 | 162 | var fromAddr = accounts[0]; // default owner address of client 163 | var toAddr = newAddress; 164 | var valueEth = amt; 165 | var value = parseFloat(valueEth)*1.0e18; 166 | var gasPrice = 1000000000000; 167 | var gas = 50000; 168 | web3.eth.sendTransaction({from: fromAddr, to: toAddr, value: value}, function (err, txhash) { 169 | if (err) console.log('ERROR: ' + err) 170 | console.log('txhash: ' + txhash + " (" + amt + " in ETH sent)"); 171 | $("#balance").html(getBalance(toAddr)); 172 | }); 173 | } 174 | 175 | window.onload = function() { 176 | 177 | web3.eth.getAccounts(function(err, accs) { 178 | if (err != null) { 179 | alert("There was an error fetching your accounts."); 180 | return; 181 | } 182 | if (accs.length == 0) { 183 | alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly."); 184 | return; 185 | } 186 | accounts = accs; 187 | account = accounts[0]; 188 | 189 | initializeConference(); 190 | }); 191 | 192 | // Wire up the UI elements 193 | $("#changeQuota").click(function() { 194 | var val = $("#confQuota").val(); 195 | changeQuota(val); 196 | }); 197 | 198 | $("#buyTicket").click(function() { 199 | var val = $("#ticketPrice").val(); 200 | var buyerAddress = $("#buyerAddress").val(); 201 | buyTicket(buyerAddress, web3.toWei(val)); 202 | }); 203 | 204 | $("#refundTicket").click(function() { 205 | var val = $("#ticketPrice").val(); 206 | var buyerAddress = $("#refBuyerAddress").val(); 207 | refundTicket(buyerAddress, web3.toWei(val)); 208 | }); 209 | 210 | $("#createWallet").click(function() { 211 | var val = $("#password").val(); 212 | if (!val) { 213 | $("#password").val("PASSWORD NEEDED").css("color", "red"); 214 | $("#password").click(function() { 215 | $("#password").val("").css("color", "black"); 216 | }); 217 | } else { 218 | createWallet(val); 219 | } 220 | }); 221 | 222 | $("#fundWallet").click(function() { 223 | var address = $("#wallet").html(); 224 | fundEth(address, 1); 225 | }); 226 | 227 | $("#checkBalance").click(function() { 228 | var address = $("#wallet").html(); 229 | $("#balance").html(getBalance(address)); 230 | }); 231 | 232 | // Set value of wallet to accounts[1] 233 | $("#buyerAddress").val(accounts[1]); 234 | $("#refBuyerAddress").val(accounts[1]); 235 | 236 | }; 237 | -------------------------------------------------------------------------------- /app/javascripts/hooked-web3-provider.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function _inherits(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var _createClass=function(){function t(t,e){for(var r=0;r=r.length)return o();var i=r[e],s=function(t){return null!=t?o(t):a.rewritePayloads(e+1,r,n,o)};if("eth_sendTransaction"!=i.method)return s();var c=i.params[0],l=c.from;this.transaction_signer.hasAddress(l,function(e,r){if(null!=e||0==r)return s(e);var u=function(e){var r=n[l];null!=r?e(null,r):a.sendAsync({jsonrpc:"2.0",method:"eth_getTransactionCount",params:[l,"pending"],id:(new Date).getTime()},function(r,n){if(null!=r)e(r);else{var o=n.result;e(null,t.prototype.toDecimal(o))}})};u(function(e,r){if(null!=e)return o(e);var u=Math.max(r,a.global_nonces[l]||0);c.nonce=t.prototype.toHex(u),n[l]=u+1,a.global_nonces[l]=u+1,a.transaction_signer.signTransaction(c,function(t,e){return null!=t?s(t):(i.method="eth_sendRawTransaction",i.params=[e],s())})})})}}]),r}(t.providers.HttpProvider);return e};"undefined"!=typeof module?module.exports=factory(require("web3")):window.HookedWeb3Provider=factory(Web3); -------------------------------------------------------------------------------- /app/stylesheets/app.css: -------------------------------------------------------------------------------- 1 | .section { 2 | margin: 20px; 3 | } -------------------------------------------------------------------------------- /contracts/Conference.sol: -------------------------------------------------------------------------------- 1 | contract Conference { // can be killed, so the owner gets sent the money in the end 2 | 3 | address public organizer; 4 | mapping (address => uint) public registrantsPaid; 5 | uint public numRegistrants; 6 | uint public quota; 7 | 8 | event Deposit(address _from, uint _amount); // so you can log the event 9 | event Refund(address _to, uint _amount); // so you can log the event 10 | 11 | function Conference() { 12 | organizer = msg.sender; 13 | quota = 100; 14 | numRegistrants = 0; 15 | } 16 | 17 | function buyTicket() public { 18 | if (numRegistrants >= quota) { 19 | throw; // throw ensures funds will be returned 20 | } 21 | registrantsPaid[msg.sender] = msg.value; 22 | numRegistrants++; 23 | Deposit(msg.sender, msg.value); 24 | } 25 | 26 | function changeQuota(uint newquota) public { 27 | if (msg.sender != organizer) { return; } 28 | quota = newquota; 29 | } 30 | 31 | function refundTicket(address recipient, uint amount) public { 32 | if (msg.sender != organizer) { return; } 33 | if (registrantsPaid[recipient] == amount) { 34 | address myAddress = this; 35 | if (myAddress.balance >= amount) { 36 | recipient.send(amount); 37 | Refund(recipient, amount); 38 | registrantsPaid[recipient] = 0; 39 | numRegistrants--; 40 | } 41 | } 42 | return; 43 | } 44 | 45 | function destroy() { 46 | if (msg.sender == organizer) { // without this funds could be locked in the contract forever! 47 | suicide(organizer); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | contract Migrations { 2 | address public owner; 3 | uint public last_completed_migration; 4 | 5 | modifier restricted() { 6 | if (msg.sender == owner) _ 7 | } 8 | 9 | function Migrations() { 10 | owner = msg.sender; 11 | } 12 | 13 | function setCompleted(uint completed) restricted { 14 | last_completed_migration = completed; 15 | } 16 | 17 | function upgrade(address new_address) restricted { 18 | Migrations upgraded = Migrations(new_address); 19 | upgraded.setCompleted(last_completed_migration); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Migrations); 3 | }; 4 | -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) { 2 | deployer.deploy(Conference); 3 | //deployer.autolink(); // for linking imports of other contracts 4 | }; 5 | -------------------------------------------------------------------------------- /test-genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "nonce": "0x0000000000000042", 3 | "difficulty": "0x1", 4 | "alloc": { 5 | "851a33107f457d8966098acaf6569df960bace09": { 6 | "balance": "20000009800000000000000000000" 7 | }, 8 | "f581b9e9487c9388706bc90b77cdac32111a018c": { 9 | "balance": "20000009800000000000000000000" 10 | } 11 | }, 12 | "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", 13 | "coinbase": "0x0000000000000000000000000000000000000000", 14 | "timestamp": "0x00", 15 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", 16 | "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", 17 | "gasLimit": "0xb2d05e00" 18 | } 19 | -------------------------------------------------------------------------------- /test/conference.js: -------------------------------------------------------------------------------- 1 | contract('Conference', function(accounts) { 2 | console.log(accounts); 3 | var owner_account = accounts[0]; 4 | var sender_account = accounts[1]; 5 | 6 | 7 | it("Initial conference settings should match", function(done) { 8 | 9 | Conference.new({from: owner_account}).then( 10 | function(conference) { 11 | conference.quota.call().then( 12 | function(quota) { 13 | assert.equal(quota, 100, "Quota doesn't match!"); 14 | }).then( 15 | function() { 16 | return conference.numRegistrants.call(); 17 | }).then( 18 | function(num) { 19 | assert.equal(num, 0, "Registrants doesn't match!"); 20 | return conference.organizer.call(); 21 | }).then( 22 | function(organizer) { 23 | assert.equal(organizer, owner_account, "Owner doesn't match!"); 24 | done(); 25 | }).catch(done); 26 | }).catch(done); 27 | }); 28 | 29 | it("Should update quota", function(done) { 30 | 31 | Conference.new({from: owner_account}).then( 32 | function(conference) { 33 | conference.quota.call().then( 34 | function(quota) { 35 | assert.equal(quota, 100, "Quota doesn't match!"); 36 | }).then( 37 | function() { 38 | return conference.changeQuota(300); 39 | }).then( 40 | function() { 41 | return conference.quota.call() 42 | }).then( 43 | function(quota) { 44 | assert.equal(quota, 300, "New quota is not correct!"); 45 | done(); 46 | }).catch(done); 47 | }).catch(done); 48 | }); 49 | 50 | 51 | it("Should let you buy a ticket", function(done) { 52 | 53 | Conference.new({ from: accounts[0] }).then( 54 | function(conference) { 55 | 56 | var ticketPrice = web3.toWei(.05, 'ether'); 57 | var initialBalance = web3.eth.getBalance(conference.address).toNumber(); 58 | 59 | conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( 60 | function() { 61 | var newBalance = web3.eth.getBalance(conference.address).toNumber(); 62 | var difference = newBalance - initialBalance; 63 | assert.equal(difference, ticketPrice, "Difference should be what was sent"); 64 | return conference.numRegistrants.call(); 65 | }).then( 66 | function(num) { 67 | assert.equal(num, 1, "there should be 1 registrant"); 68 | return conference.registrantsPaid.call(sender_account); 69 | }).then( 70 | function(amount) { 71 | assert.equal(amount.toNumber(), ticketPrice, "Sender's paid but is not listed as paying"); 72 | return web3.eth.getBalance(conference.address); 73 | }).then( 74 | function(bal) { 75 | assert.equal(bal.toNumber(), ticketPrice, "Final balance mismatch"); 76 | done(); 77 | }).catch(done); 78 | }).catch(done); 79 | }); 80 | 81 | it("Should issue a refund by owner only", function(done) { 82 | 83 | Conference.new({ from: accounts[0] }).then( 84 | function(conference) { 85 | 86 | var ticketPrice = web3.toWei(.05, 'ether'); 87 | var initialBalance = web3.eth.getBalance(conference.address).toNumber(); 88 | 89 | conference.buyTicket({ from: accounts[1], value: ticketPrice }).then( 90 | function() { 91 | var newBalance = web3.eth.getBalance(conference.address).toNumber(); 92 | var difference = newBalance - initialBalance; 93 | assert.equal(difference, ticketPrice, "Difference should be what was sent"); 94 | 95 | // Now try to issue refund as second user - should fail 96 | return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[1]}); 97 | }).then( 98 | function() { 99 | var balance = web3.eth.getBalance(conference.address); 100 | assert.equal(balance, ticketPrice, "Balance should be unchanged"); 101 | // Now try to issue refund as organizer/owner 102 | return conference.refundTicket(accounts[1], ticketPrice, {from: accounts[0]}); 103 | }).then( 104 | function() { 105 | var postRefundBalance = web3.eth.getBalance(conference.address).toNumber(); 106 | assert.equal(postRefundBalance, initialBalance, "Balance should be initial balance"); 107 | done(); 108 | }).catch(done); 109 | }).catch(done); 110 | }); 111 | 112 | }); 113 | 114 | -------------------------------------------------------------------------------- /truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | build: { 3 | "index.html": "index.html", 4 | "app.js": [ 5 | "javascripts/app.js" 6 | ], 7 | "app.css": [ 8 | "stylesheets/app.css" 9 | ], 10 | "images/": "images/" 11 | }, 12 | rpc: { 13 | host: "localhost", 14 | port: 8545 15 | } 16 | }; 17 | --------------------------------------------------------------------------------