├── .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 |
--------------------------------------------------------------------------------