diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..e7a90cf
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "./bower_components"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3695252
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+logs/*
+!.gitkeep
+node_modules/
+bower_components/
+tmp
+.DS_Store
+.idea
+environments/**
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..6f00218
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,13 @@
+{
+ "globalstrict": true,
+ "globals": {
+ "angular": false,
+ "describe": false,
+ "it": false,
+ "expect": false,
+ "beforeEach": false,
+ "afterEach": false,
+ "module": false,
+ "inject": false
+ }
+}
\ No newline at end of file
diff --git a/.pack.swp b/.pack.swp
new file mode 100644
index 0000000..5f03b1e
Binary files /dev/null and b/.pack.swp differ
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..cce5c68
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: node_js
+node_js:
+ - "0.10"
+
+before_script:
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+ - npm start > /dev/null &
+ - npm run update-webdriver
+ - sleep 1 # give server time to start
+
+script:
+ - node_modules/.bin/karma start karma.conf.js --no-auto-watch --single-run --reporters=dots --browsers=Firefox
+ - node_modules/.bin/protractor e2e-tests/protractor.conf.js --browser=firefox
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9ced331
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) 2010-2014 Google, Inc. http://angularjs.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/README.md b/README.md
index 4ffa539..037a3fa 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,113 @@
# KimberlyProcessEthereum
-An implementation of The Kimberley Process built on Ethereum
+An implementation of The Kimberley Process's certificate issuance and validation system built on the Ethereum blockchain.
+
+---
+
+__My complete thoughts on diamonds, the diamond industry and The Kimberley Process are available here:__
+[http://romancingthestones.diamonds](http://romancingthestones.diamonds)
+
+---
+
+##About
+
+[The Kimberley Process](http://www.kimberleyprocess.com) (KP) is a joint government, industry and civil society initiative to stem the flow of conflict diamonds – rough diamonds used by rebel movements to finance wars against legitimate governments.
+
+Implementation of the KP (including definitions) is outlined in the [KPCS Core Document](http://www.kimberleyprocess.com/en/kpcs-core-document).
+
+In order to bring both transparency and integrity to the KP, my proposal is to put the entirety of the certificate issuance process, including all participants, authorities, observers, agents and parties, on the Ethereum blockchain. The specific features of the blockchain (mathematically-enforced security, immutability, public availability of data) would be a dramatic improvement over the current system of paper certificates and their known weaknesses, such as susceptibility to forgery, varying certificate security features, and overly-generous expiration dates (and therefore the possibility of re-use).
+
+---
+
+##Definitions
+
+###Participant
+Member countries that are participants in The Kimberley Process.
+[Here is a current list of participant countries](http://www.kimberleyprocess.com/en/participants)
+
+###Particpant Authority
+An authority designated by the Particpant with the power to issue certificates. For example, the Ministry of Mines and Mineral Resources (Sierra Leone). These authorities do not issue certificates directly, but rather delegate those powers to specific employees, or agents.
+
+###Participant Agent
+An individual designated by a Participant Authority with the power to issue certificates.
+
+###Party
+An entity or individual acting as either the source or destination of the shipment of rough diamonds over international borders.
+
+---
+
+##Certificate issuance process:
+
+###1. A certificate is created by the exporting party, with references to:
+- the importing party we are sending the shipment to
+- the source participant
+- the destination participant
+
+```solidity
+//Certificate.sol:
+function Certificate(address _exporter,
+ address _importer,
+ address _participantSource,
+ address _participantDestination) {
+ ...
+}
+```
+
+Each certificate contains the following data:
+
+1. Participants
+ - _Origins:_ geological origins where goods were mined from. _Note: This value is not set directly, but is derived from the origins of the parsels._
+ - _Source:_ participant country the shipment is being sent from.
+ - _Destination:_ participant country the shipment is being sent to.
+
+2. Agents
+ - _Exporting:_ agent delegated by source participant's exporting authority the power to sign certificates.
+ - _Importing:_ agent delegated by destination participant's importing authority the power to sign certificates.
+
+3. Parties
+ - _Exporting:_ entity or individual goods are being sent from
+ - _Importing:_ entity or individual goods are being sent to
+
+4. Parsels
+ - An array consisting of parsels of goods included in the shipment. Data includes:
+ - carats
+ - value
+ - geological origins
+
+###2. Parsels are added to the certificate:
+```solidity
+//Certificate.sol
+function addParsel(uint carats,
+ uint value,
+ address[] origins) {
+ ...
+}
+```
+Each parsel contains:
+- Carats
+- Assessed value
+- Addresses of participant countries of geological origins
+
+###3. Signatures required from:
+1. Importing party
+2. Exporting authority agent
+3. Importing authority agent
+
+```solidity
+//Certificate.sol
+function sign() {
+ ...
+}
+```
+
+Upon receipt of the last required signature (order is unimportant), the certificate is issued and shipment is cleared for transit.
+
+###4. Importing authority marks shipment as received:
+
+```solidity
+//Certificate.sol:
+function markAsReceived() {
+ ...
+}
+```
+
+Upon acknowleged receipt of the shipment, the importing authority marks the certificate as received, completing the certificate.
diff --git a/app/app.js b/app/app.js
new file mode 100644
index 0000000..a4cbada
--- /dev/null
+++ b/app/app.js
@@ -0,0 +1,14 @@
+(function() {
+ 'use strict';
+
+ angular.module('kpcs', [
+ 'ngRoute',
+ 'rzModule',
+ 'oitozero.ngSweetAlert',
+ 'angularMoment'
+ ]).
+ config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
+ $routeProvider.otherwise({redirectTo: '/'});
+ $locationProvider.html5Mode(true);
+ }]);
+})();
diff --git a/app/ethereum.service.js b/app/ethereum.service.js
new file mode 100644
index 0000000..e33fc96
--- /dev/null
+++ b/app/ethereum.service.js
@@ -0,0 +1,27 @@
+(function() {
+ 'use strict';
+
+ angular.module('kpcs').service('ethereum', ethereum);
+
+ /* @ngInject */
+ function ethereum() {
+ var service = {
+ web3: new Web3(),
+ isConnected: isConnected
+ };
+
+ activate();
+
+ return service;
+
+ ///////////////////
+
+ function activate() {
+ service.web3.setProvider(new service.web3.providers.HttpProvider("http://localhost:8545"));
+ }
+
+ function isConnected() {
+ return service.web3.currentProvider.isConnected();
+ }
+ }
+})();
diff --git a/app/index.html b/app/index.html
new file mode 100644
index 0000000..5bec170
--- /dev/null
+++ b/app/index.html
@@ -0,0 +1,22 @@
+
+
+
+ MetaCoin - Default Truffle App
+
+
+
+
+
+
MetaCoin
+
Example Truffle Dapp
+
You have META
+
+
+
Send
+
+
+
+
+
+
+
diff --git a/app/javascripts/app.js b/app/javascripts/app.js
new file mode 100644
index 0000000..c422ac0
--- /dev/null
+++ b/app/javascripts/app.js
@@ -0,0 +1,56 @@
+var accounts;
+var account;
+var balance;
+
+function setStatus(message) {
+ var status = document.getElementById("status");
+ status.innerHTML = message;
+};
+
+function refreshBalance() {
+ var meta = MetaCoin.deployed();
+
+ meta.getBalance.call(account, {from: account}).then(function(value) {
+ var balance_element = document.getElementById("balance");
+ balance_element.innerHTML = value.valueOf();
+ }).catch(function(e) {
+ console.log(e);
+ setStatus("Error getting balance; see log.");
+ });
+};
+
+function sendCoin() {
+ var meta = MetaCoin.deployed();
+
+ var amount = parseInt(document.getElementById("amount").value);
+ var receiver = document.getElementById("receiver").value;
+
+ setStatus("Initiating transaction... (please wait)");
+
+ meta.sendCoin(receiver, amount, {from: account}).then(function() {
+ setStatus("Transaction complete!");
+ refreshBalance();
+ }).catch(function(e) {
+ console.log(e);
+ setStatus("Error sending coin; see log.");
+ });
+};
+
+window.onload = function() {
+ web3.eth.getAccounts(function(err, accs) {
+ if (err != null) {
+ alert("There was an error fetching your accounts.");
+ return;
+ }
+
+ if (accs.length == 0) {
+ alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.");
+ return;
+ }
+
+ accounts = accs;
+ account = accounts[0];
+
+ refreshBalance();
+ });
+}
diff --git a/app/stylesheets/app.css b/app/stylesheets/app.css
new file mode 100644
index 0000000..7fdad0b
--- /dev/null
+++ b/app/stylesheets/app.css
@@ -0,0 +1,48 @@
+body {
+ margin-left: 25%;
+ margin-right: 25%;
+ margin-top: 10%;
+ font-family: "Open Sans", sans-serif;
+}
+
+label {
+ display: inline-block;
+ width: 100px;
+}
+
+input {
+ width: 500px;
+ padding: 5px;
+ font-size: 16px;
+}
+
+button {
+ font-size: 16px;
+ padding: 5px;
+}
+
+h1, h2 {
+ display: inline-block;
+ vertical-align: middle;
+ margin-top: 0px;
+ margin-bottom: 10px;
+}
+
+h2 {
+ color: #AAA;
+ font-size: 32px;
+}
+
+h3 {
+ font-weight: normal;
+ color: #AAA;
+ font-size: 24px;
+}
+
+.black {
+ color: black;
+}
+
+#balance {
+ color: black;
+}
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..e993dc4
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,22 @@
+{
+ "name": "kpcs",
+ "description": "A simple Ethereum dapp that allows accounts to contribute ether to be redistributed using a weighted random selection.",
+ "version": "0.0.0",
+ "license": "MIT",
+ "private": true,
+ "dependencies": {
+ "angular-route": "~1.4.0",
+ "angular-loader": "~1.4.0",
+ "html5-boilerplate": "~5.2.0",
+ "angular-bootstrap": "~1.2.4",
+ "web3": "~0.15.3",
+ "angularjs-slider": "~2.10.2",
+ "Chart.js": "~1.0.2",
+ "moment": "~2.12.0",
+ "ngSweetAlert": "angular-sweetalert#~1.1.0",
+ "angular-moment": "~0.10.3"
+ },
+ "ignoredDependencies": [
+ "angular"
+ ]
+}
diff --git a/contracts/Administrator.sol b/contracts/Administrator.sol
new file mode 100644
index 0000000..b678676
--- /dev/null
+++ b/contracts/Administrator.sol
@@ -0,0 +1,13 @@
+import {User} from "./User.sol";
+
+contract Administrator is User("name", 0x0) {
+ function Administrator(string _name, address _administrator) {
+ name = _name;
+ owner = msg.sender;
+ administrator = _administrator;
+ }
+
+ function getType() returns (int) {
+ return -1;
+ }
+}
diff --git a/contracts/Certificate.sol b/contracts/Certificate.sol
new file mode 100644
index 0000000..0757305
--- /dev/null
+++ b/contracts/Certificate.sol
@@ -0,0 +1,251 @@
+import {User} from "./User.sol";
+import {Participant} from "./Participant.sol";
+import {ParticipantAuthority} from "./ParticipantAuthority.sol";
+import {UserState} from "./UserState.sol";
+
+contract Certificate {
+
+ address public owner;
+
+ enum State {
+ /*
+ Pending: certificate has been created, but awaiting signatures from issuer and either importer or exporter
+ Issued: all signatures received, certificate is valid.
+ Completed: shipment validated upon border crossing
+ Expired: shipment has expired without receiving a 'completed' Event
+ */
+ Pending, Issued, Completed, Expired
+ }
+ State private state = State.Pending;
+
+ struct Dates {
+ //date the certificate is created + requested
+ uint created;
+
+ //date the certificate is signed by all parties and is officially issued, becoming valid
+ uint issued;
+
+ //date the shipment is certified by the importing authority
+ uint completed;
+
+ //default expiration date of the certificate, exercised only if shipment never verified by importing authority
+ uint expired;
+ }
+ Dates public dates = Dates(now, 0, 0, 0);
+
+ //Participants in the KP: member countries of source and destination
+ struct Participants {
+ address[] origins; //the declared origin of the goods
+ address source; //the country we are exporting from
+ address destination; //the country we are importing to
+ }
+ Participants private participants;
+
+ //the parties to the transaction: importer and exporter
+ struct Parties {
+ address exporter;
+ address importer;
+ }
+ Parties public parties;
+
+ struct Signature {
+ uint date;
+ address owner;
+ }
+
+ struct Signatures {
+ Signature exporter;
+ Signature importer;
+ Signature exporterAuthority;
+ Signature importerAuthority;
+ Signature importerAuthorityOnReceipt;
+ }
+ Signatures private signatures;
+
+ struct Parsel {
+ uint carats;
+ uint value;
+ address[] origins;
+ }
+ Parsel[] public parsels;
+
+ event Requested(address indexed certificate);
+ event Issued(address indexed certificate);
+ event Expired(address indexed certificate);
+ event Exported(address indexed certificate);
+ event Imported(address indexed certificate);
+ event Signed(address from, string name);
+ event Complete(address from, string name);
+
+ /*
+ Certificates should be created by the exporter: the party in possession of the goods.
+ params:
+ - importer - importing Party
+ - exporter - exporting Party
+ - participantOrigin - KPCS Participant (member country) the goods were sourced _from_ ... likely the country of geological origin
+ - participantSource - KPCS Participant (member country) the goods are being sent from
+ - participantDestination - KPCS Participant (member country) the goods are being sent to
+ */
+ function Certificate(address _exporter,
+ address _importer,
+ address _participantSource,
+ address _participantDestination) {
+ owner = msg.sender;
+ parties = Parties(_exporter, _importer);
+
+ if(User(parties.exporter).getState() == UserState.Accepted()) {
+ participants = Participants(new address[](0x0), _participantSource, _participantDestination);
+ owner = msg.sender;
+ signatures = Signatures(
+ Signature(now, _exporter),
+ Signature(0,0x0),
+ Signature(0,0x0),
+ Signature(0,0x0),
+ Signature(0,0x0));
+ dates = Dates(now, 0, 0, now + (60 * 60 * 24 * 30));
+ }
+ }
+
+ function getNumberOfParticipantsOrigins() constant returns (uint) {
+ return participants.origins.length;
+ }
+
+ function getParticipantOriginWithIndex(uint index) constant returns (address) {
+ return participants.origins[index];
+ }
+
+ function getParticipantSource() constant returns (address) {
+ return participants.source;
+ }
+
+ function getParticipantDestination() constant returns (address) {
+ return participants.destination;
+ }
+
+ function getParticipants() constant returns (address[]) {
+ address[] memory allParticipants = new address[](participants.origins.length + 2);
+ allParticipants[0] = (participants.source);
+ allParticipants[1] = participants.destination;
+ uint origins = participants.origins.length;
+ for(uint i = 0; i 0) {
+ return false;
+ }
+ if(Participant(participants.source).getState() != UserState.Accepted()) {
+ return false;
+ }
+ return true;
+ } else if(ParticipantAuthority(Participant(participants.destination).getImportingAuthority()).isSenderRegisteredAgent(msg.sender)) {
+ if(signatures.importerAuthority.date > 0) {
+ return false;
+ }
+ if(Participant(participants.destination).getState() != UserState.Accepted()) {
+ return false;
+ }
+ return true;
+ } else if(msg.sender == User(parties.importer).owner()) {
+ if(signatures.importer.date > 0 || User(parties.importer).getState() != UserState.Accepted()) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ function sign() {
+ if(ParticipantAuthority(Participant(participants.source).getExportingAuthority()).isSenderRegisteredAgent(msg.sender)) {
+ if(signatures.exporterAuthority.date > 0) {
+ return;
+ }
+ if(Participant(participants.source).getState() != UserState.Accepted()) {
+ return;
+ }
+ Signed(msg.sender, "Exporting Authority");
+ signatures.exporterAuthority = Signature(now, msg.sender);
+ } else if(ParticipantAuthority(Participant(participants.destination).getImportingAuthority()).isSenderRegisteredAgent(msg.sender)) {
+ if(signatures.importerAuthority.date > 0) {
+ return;
+ }
+ if(Participant(participants.destination).getState() != UserState.Accepted()) {
+ return;
+ }
+ Signed(msg.sender, "Importing Authority");
+ signatures.importerAuthority = Signature(now, msg.sender);
+ } else if(msg.sender == User(parties.importer).owner()) {
+ if(signatures.importer.date > 0 || User(parties.importer).getState() != UserState.Accepted()) {
+ return;
+ }
+ Signed(msg.sender, "Importing Party");
+ signatures.importer = Signature(now, msg.sender);
+ } else {
+ return;
+ }
+
+ if(hasRequiredSignaturesToValidate()) {
+ state = State.Issued;
+ dates.issued = now;
+ Issued(this);
+ }
+ }
+
+ function markAsReceived() {
+ if(ParticipantAuthority(Participant(participants.destination).getImportingAuthority()).isSenderRegisteredAgent(msg.sender)) {
+ if(signatures.importerAuthorityOnReceipt.date > 0) {
+ return;
+ }
+ dates.completed = now;
+ signatures.importerAuthorityOnReceipt = Signature(now, msg.sender);
+ state = State.Completed;
+ Complete(msg.sender, "Importing Authority - On Receipt");
+ }
+ }
+
+ function hasRequiredSignaturesToValidate() returns (bool isComplete) {
+ return (signatures.importerAuthority.date > 0 && signatures.exporterAuthority.date > 0 && signatures.importer.date > 0 && signatures.exporter.date > 0);
+ }
+
+ function isExpired() returns (bool) {
+ return (state == State.Expired);
+ }
+
+ function isComplete() returns (bool) {
+ return (state == State.Completed);
+ }
+
+ function isValid() returns (bool) {
+ return (state == State.Issued && now < dates.expired);
+ }
+}
diff --git a/contracts/KPCS.sol b/contracts/KPCS.sol
new file mode 100644
index 0000000..24be24b
--- /dev/null
+++ b/contracts/KPCS.sol
@@ -0,0 +1,70 @@
+import {User} from "./User.sol";
+import {UserType} from "./UserType.sol";
+import {Certificate} from "./Certificate.sol";
+import {Participant} from "./Participant.sol";
+import {Party} from "./Party.sol";
+
+contract KPCS {
+
+ event CertificateIssued(address _certificate);
+
+ address private owner;
+
+ address public administrator;
+
+ //all certificates
+ mapping(address => address) public certificates;
+
+ //member countries
+ mapping(address => address) public participants;
+
+ //member countries
+ mapping(address => address) public parties;
+
+ event ParticipantRegistered(address participant);
+
+ function KPCS(address _administrator) {
+ administrator = _administrator;
+ owner = msg.sender;
+ }
+
+ function kill() {
+ if (msg.sender == owner) suicide(owner);
+ }
+
+ function registerAsParty(address _party) {
+ if(msg.sender != owner) {
+ return;
+ }
+ if(User(_party).getType() != UserType.Party() || parties[_party] != 0x0) {
+ return;
+ }
+ parties[_party] = _party;
+ }
+
+ function isCertificateRegisteredAndValid(address certificate) returns (bool) {
+ return (Certificate(certificate).isValid() && certificates[certificate] != 0x0);
+ }
+
+ function registerCertificate(address _certificate) {
+ Certificate certificate = Certificate(_certificate);
+ if(certificate.isValid()) {
+ certificates[_certificate] = certificate;
+ CertificateIssued(_certificate);
+ return;
+ }
+ throw;
+ }
+
+ function registerParticipant(address participant) {
+ if(msg.sender != owner) {
+ throw;
+ }
+ participants[participant] = participant;
+ ParticipantRegistered(participant);
+ }
+
+ function participantCanParticipate(address participant) returns (bool) {
+ return participants[participant] != 0x0;
+ }
+}
diff --git a/contracts/KPCSAdministrator.sol b/contracts/KPCSAdministrator.sol
new file mode 100644
index 0000000..72c98d0
--- /dev/null
+++ b/contracts/KPCSAdministrator.sol
@@ -0,0 +1,9 @@
+import {Administrator} from "./Administrator.sol";
+
+contract KPCSAdministrator is Administrator("name", 0x0) {
+ function KPCSAdministrator(string _name) {
+ name = _name;
+ owner = msg.sender;
+ state = State.Accepted;
+ }
+}
diff --git a/contracts/Participant.sol b/contracts/Participant.sol
new file mode 100644
index 0000000..de24bcb
--- /dev/null
+++ b/contracts/Participant.sol
@@ -0,0 +1,70 @@
+import {Administrator} from "./Administrator.sol";
+import {UserType, User} from "./User.sol";
+import {ParticipantAuthority} from "./ParticipantAuthority.sol";
+
+/*
+ Participants are member countries that participate in the Kimberley Process.
+ They delegate ParticipantAgents, which are entities (e.g. the Minister of Mines
+ and Mining), which have the power to issue certificates
+*/
+contract Participant is Administrator("name", 0x0) {
+ mapping(address => bool) public registeredAddresses;
+
+ event ParticipantCreated(address participant, string name, address administrator);
+
+ //KPCS Core Document - IV Each particpant should: (b) designate an Importing and an Exporting Authority(ies);
+ struct Authorities {
+ address importing;
+ address exporting;
+ }
+ Authorities private authorities;
+
+ event ImportingAuthorityAcceptedRegistered(address authority);
+ event ExportingAuthorityAcceptedRegistered(address authority);
+
+ function Participant(string _name, address _administrator) {
+ name = _name;
+ administrator = _administrator;
+ owner = msg.sender;
+ ParticipantCreated(this, name, administrator);
+ authorities = Authorities(0x0, 0x0);
+ }
+
+ function getType() returns (int) {
+ return UserType.Participant();
+ }
+
+ function getImportingAuthority() returns (address) {
+ return authorities.importing;
+ }
+
+ function getExportingAuthority() returns (address) {
+ return authorities.exporting;
+ }
+
+ function isAcceptedImportingAuthority(address authority) returns (bool) {
+ return authorities.importing == authority;
+ }
+
+ function isAcceptedExportingAuthority(address authority) returns (bool) {
+ return authorities.exporting == authority;
+ }
+
+ function registerAsImportingAuthority(address authority) returns (bool) {
+ if(msg.sender != owner || authorities.importing != 0x0) {
+ return false;
+ }
+ authorities.importing = authority;
+ ImportingAuthorityAcceptedRegistered(authority);
+ return true;
+ }
+
+ function registerAsExportingAuthority(address authority) returns (bool) {
+ if(msg.sender != owner || authorities.exporting != 0x0) {
+ return false;
+ }
+ authorities.exporting = authority;
+ ExportingAuthorityAcceptedRegistered(authority);
+ return true;
+ }
+}
diff --git a/contracts/ParticipantAgent.sol b/contracts/ParticipantAgent.sol
new file mode 100644
index 0000000..5e88a3b
--- /dev/null
+++ b/contracts/ParticipantAgent.sol
@@ -0,0 +1,25 @@
+import {Administrator} from "./Administrator.sol";
+import {UserType} from "./UserType.sol";
+
+contract ParticipantAgent is Administrator("name", 0x0) {
+ /*
+ ParticipantAgents are entities delegated by Participants
+ (e.g. the Minister of Mines and Mining) the power to
+ issue certificates.
+
+ These individuals can act on behalf of the Particiapnt to:
+ 1. sign certificates
+ 2. managed Party entities
+
+ */
+
+ function ParticipantAgent(string _name, address _administrator) {
+ name = _name;
+ owner = msg.sender;
+ administrator = _administrator;
+ }
+
+ function getType() returns (int) {
+ return UserType.ParticipantAgent();
+ }
+}
diff --git a/contracts/ParticipantAuthority.sol b/contracts/ParticipantAuthority.sol
new file mode 100644
index 0000000..d7fb9d6
--- /dev/null
+++ b/contracts/ParticipantAuthority.sol
@@ -0,0 +1,39 @@
+import {Administrator} from "./Administrator.sol";
+import {UserType} from "./UserType.sol";
+import {User} from "./User.sol";
+
+/*
+ ParticipantAuthorities are entities (e.g. The Minister of Mines and Mining),
+ designated by a Participant (country) as having the power to issue certificates,
+ which is a responsibility that they delegate to their agents acting on their behalf
+ (e.g. an employee at the Minister of Mines and Mining).
+*/
+
+contract ParticipantAuthority is Administrator("name", 0x0) {
+
+ mapping(address => bool) private agents;
+
+ event ParticipantAgentRegistered(address agent);
+
+ function ParticipantAuthority(string _name, address _administrator) {
+ name = _name;
+ owner = msg.sender;
+ administrator = _administrator;
+ }
+
+ function isSenderRegisteredAgent(address sender) returns (bool) {
+ return agents[sender] == true;
+ }
+
+ function registerParticipantAgent(address agent) {
+ if(msg.sender != owner || agents[address(User(agent).owner())] == true || User(agent).state() != State.Accepted) {
+ return;
+ }
+ agents[address(User(agent).owner())] = true;
+ ParticipantAgentRegistered(agent);
+ }
+
+ function getType() returns (int) {
+ return UserType.ParticipantAuthority();
+ }
+}
diff --git a/contracts/Party.sol b/contracts/Party.sol
new file mode 100644
index 0000000..fa1c9a3
--- /dev/null
+++ b/contracts/Party.sol
@@ -0,0 +1,18 @@
+import {User} from "./User.sol";
+import {UserType} from "./UserType.sol";
+
+contract Party is User("name", 0x0) {
+ string public contactDetails;
+
+ function Party(string _name, address _administrator, string _contactDetails) {
+ name = _name;
+ owner = msg.sender;
+ contactDetails = _contactDetails;
+ state = State.Applied;
+ administrator = _administrator;
+ }
+
+ function getType() returns (int) {
+ return UserType.Party();
+ }
+}
diff --git a/contracts/User.sol b/contracts/User.sol
new file mode 100644
index 0000000..56de93c
--- /dev/null
+++ b/contracts/User.sol
@@ -0,0 +1,69 @@
+import {UserType} from "./UserType.sol";
+
+contract User {
+ enum State {
+ Applied, Accepted, Rejected, Suspended
+ }
+ State public state;
+
+ event UserStateChanged(address user, State state, address administrator);
+
+ address public owner;
+ address public administrator;
+ string public name;
+ uint public dateCreated = now;
+
+ function User(string _name, address _administrator) {
+ state = State.Applied;
+ name = _name;
+ owner = msg.sender;
+ administrator = _administrator;
+ UserStateChanged(this, state, administrator);
+ }
+
+ function setState(uint _state) returns (bool) {
+ //user cannot change their own status, can only be done by the issuing administrator
+ if(msg.sender != administrator) {
+ return false;
+ }
+ state = State(_state);
+ return true;
+ }
+
+ function getName() returns (string) {
+ return name;
+ }
+
+ function getType() returns (int) {
+ throw;
+ }
+
+ function getState() returns (uint) {
+ return uint(state);
+ }
+
+ function accept() {
+ if(msg.sender != User(administrator).owner()) {
+ return;
+ }
+ state = State.Accepted;
+ }
+
+ function reject() {
+ if(msg.sender != User(administrator).owner()) {
+ return;
+ }
+ state = State.Rejected;
+ }
+
+ function suspend() {
+ if(msg.sender != User(administrator).owner()) {
+ return;
+ }
+ state = State.Suspended;
+ }
+
+ function kill() {
+ if (msg.sender == owner) suicide(owner);
+ }
+}
diff --git a/contracts/UserState.sol b/contracts/UserState.sol
new file mode 100644
index 0000000..49bdc64
--- /dev/null
+++ b/contracts/UserState.sol
@@ -0,0 +1,17 @@
+library UserState {
+ function Applied() returns (uint) {
+ return 0;
+ }
+
+ function Accepted() returns (uint) {
+ return 1;
+ }
+
+ function Rejected() returns (uint) {
+ return 2;
+ }
+
+ function Suspended() returns (uint) {
+ return 3;
+ }
+}
diff --git a/contracts/UserType.sol b/contracts/UserType.sol
new file mode 100644
index 0000000..438b99d
--- /dev/null
+++ b/contracts/UserType.sol
@@ -0,0 +1,21 @@
+library UserType {
+ function Party() returns (int) {
+ return 0;
+ }
+
+ function Participant() returns (int) {
+ return 1;
+ }
+
+ function ParticipantAuthority() returns (int) {
+ return 2;
+ }
+
+ function ParticipantAgent() returns (int) {
+ return 3;
+ }
+
+ function KPCSAdministrator() returns (int) {
+ return 4;
+ }
+}
diff --git a/environments/development/contracts/Party.sol.js b/environments/development/contracts/Party.sol.js
new file mode 100644
index 0000000..32ebc15
--- /dev/null
+++ b/environments/development/contracts/Party.sol.js
@@ -0,0 +1,65 @@
+// Factory "morphs" into a Pudding class.
+// The reasoning is that calling load in each context
+// is cumbersome.
+
+(function() {
+
+ var contract_data = {
+ abi: [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":false,"inputs":[],"name":"getType","outputs":[{"name":"","type":"int256"}],"type":"function"},{"constant":false,"inputs":[],"name":"getState","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"dateCreated","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"accept","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"kill","outputs":[],"type":"function"},{"constant":false,"inputs":[],"name":"reject","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"contactDetails","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_state","type":"uint256"}],"name":"setState","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"state","outputs":[{"name":"","type":"uint8"}],"type":"function"},{"constant":false,"inputs":[],"name":"suspend","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"administrator","outputs":[{"name":"","type":"address"}],"type":"function"},{"inputs":[{"name":"_name","type":"string"},{"name":"_contactDetails","type":"string"},{"name":"_administrator","type":"address"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"user","type":"address"},{"indexed":false,"name":"state","type":"User.State"},{"indexed":false,"name":"administrator","type":"address"}],"name":"UserStateChanged","type":"event"}],
+ binary: "60606040524260036000505560405161069c38038061069c833981516080805160a0805190850160405260048587019081527f6e616d6500000000000000000000000000000000000000000000000000000000939095019283526000805460ff1990811682556002805481845295519091166008178155948701969290920194909390926100dc907f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace602061010060018416150260001901909216849004601f01919091048101905b808211156101b457600081556001016100c8565b50506000805461010060a860020a0319163361010002179081905560018054600160a060020a03191683179081905560408051600160a060020a03308116825260ff949094166020820152919092168183015290517f02341e2d1bb4aad6cc87f66d24928a0f204b5c0d0230234b34526a29f98bb79d9181900360600190a150508260026000509080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106101b857805160ff19168380011785555b506101e89291506100c8565b5090565b828001600101855582156101a8579182015b828111156101a85782518260005055916020019190600101906101ca565b505033600060016101000a815481600160a060020a03021916908302179055508160046000509080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061025f57805160ff19168380011785555b5061028f9291506100c8565b82800160010185558215610253579182015b82811115610253578251826000505591602001919060010190610271565b50506000805460ff1916905560018054600160a060020a031916821790555050506103de806102be6000396000f3606060405236156100a35760e060020a600035046306fdde0381146100a557806315dae03e146100ff5780631865c57d1461016a5780631f741c7a146101775780632852b71c1461018057806341c0e1b5146101a15780634dc415de146101d45780636e94fadc146101f55780638da5cb5b14610251578063a9e966b714610268578063c19d93fb1461028c578063e6400bbe14610298578063f53d0a8e146102b9575b005b6102cb60028054602060018216156101000260001901909116829004601f810182900490910260809081016040526060828152929190828280156103895780601f1061035e57610100808354040283529160200191610389565b6103397f361a4eb800000000000000000000000000000000000000000000000000000000606090815260009073__UserType______________________________9063361a4eb8906064906020906004818660325a03f4156100025750506040515191506101749050565b61033960005460ff165b90565b61033960035481565b61033960015460009033600160a060020a039081169116146103a357610174565b6100a36000546101009004600160a060020a039081163390911614156103dc576000546101009004600160a060020a0316ff5b61033960015460009033600160a060020a039081169116146103b657610174565b6102cb600480546020601f600260001960018516156101000201909316929092049182018190040260809081016040526060828152929190828280156103895780601f1061035e57610100808354040283529160200191610389565b61034b6000546101009004600160a060020a031681565b61033960043560015460009033600160a060020a039081169116146103915761039e565b61033960005460ff1681565b61033960015460009033600160a060020a039081169116146103ca57610174565b61034b600154600160a060020a031681565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600f02600301f150905090810190601f16801561032b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60408051918252519081900360200190f35b600160a060020a03166060908152602090f35b820191906000526020600020905b81548152906001019060200180831161036c57829003601f168201915b505050505081565b805460ff19168217905560015b919050565b805460ff19166001908117909155610174565b8054600260ff199091161790556001610174565b805460ff191660031790556001610174565b56",
+ unlinked_binary: "60606040524260036000505560405161069c38038061069c833981516080805160a0805190850160405260048587019081527f6e616d6500000000000000000000000000000000000000000000000000000000939095019283526000805460ff1990811682556002805481845295519091166008178155948701969290920194909390926100dc907f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace602061010060018416150260001901909216849004601f01919091048101905b808211156101b457600081556001016100c8565b50506000805461010060a860020a0319163361010002179081905560018054600160a060020a03191683179081905560408051600160a060020a03308116825260ff949094166020820152919092168183015290517f02341e2d1bb4aad6cc87f66d24928a0f204b5c0d0230234b34526a29f98bb79d9181900360600190a150508260026000509080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106101b857805160ff19168380011785555b506101e89291506100c8565b5090565b828001600101855582156101a8579182015b828111156101a85782518260005055916020019190600101906101ca565b505033600060016101000a815481600160a060020a03021916908302179055508160046000509080519060200190828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061025f57805160ff19168380011785555b5061028f9291506100c8565b82800160010185558215610253579182015b82811115610253578251826000505591602001919060010190610271565b50506000805460ff1916905560018054600160a060020a031916821790555050506103de806102be6000396000f3606060405236156100a35760e060020a600035046306fdde0381146100a557806315dae03e146100ff5780631865c57d1461016a5780631f741c7a146101775780632852b71c1461018057806341c0e1b5146101a15780634dc415de146101d45780636e94fadc146101f55780638da5cb5b14610251578063a9e966b714610268578063c19d93fb1461028c578063e6400bbe14610298578063f53d0a8e146102b9575b005b6102cb60028054602060018216156101000260001901909116829004601f810182900490910260809081016040526060828152929190828280156103895780601f1061035e57610100808354040283529160200191610389565b6103397f361a4eb800000000000000000000000000000000000000000000000000000000606090815260009073__UserType______________________________9063361a4eb8906064906020906004818660325a03f4156100025750506040515191506101749050565b61033960005460ff165b90565b61033960035481565b61033960015460009033600160a060020a039081169116146103a357610174565b6100a36000546101009004600160a060020a039081163390911614156103dc576000546101009004600160a060020a0316ff5b61033960015460009033600160a060020a039081169116146103b657610174565b6102cb600480546020601f600260001960018516156101000201909316929092049182018190040260809081016040526060828152929190828280156103895780601f1061035e57610100808354040283529160200191610389565b61034b6000546101009004600160a060020a031681565b61033960043560015460009033600160a060020a039081169116146103915761039e565b61033960005460ff1681565b61033960015460009033600160a060020a039081169116146103ca57610174565b61034b600154600160a060020a031681565b60405180806020018281038252838181518152602001915080519060200190808383829060006004602084601f0104600f02600301f150905090810190601f16801561032b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60408051918252519081900360200190f35b600160a060020a03166060908152602090f35b820191906000526020600020905b81548152906001019060200180831161036c57829003601f168201915b505050505081565b805460ff19168217905560015b919050565b805460ff19166001908117909155610174565b8054600260ff199091161790556001610174565b805460ff191660031790556001610174565b56",
+ address: "0x61e55511d519b25408fe307b78dda71a5c98eb4e",
+ generated_with: "2.0.6",
+ contract_name: "Party"
+ };
+
+ function Contract() {
+ if (Contract.Pudding == null) {
+ throw new Error("Party error: Please call load() first before creating new instance of this contract.");
+ }
+
+ Contract.Pudding.apply(this, arguments);
+ };
+
+ Contract.load = function(Pudding) {
+ Contract.Pudding = Pudding;
+
+ Pudding.whisk(contract_data, Contract);
+
+ // Return itself for backwards compatibility.
+ return Contract;
+ }
+
+ Contract.new = function() {
+ if (Contract.Pudding == null) {
+ throw new Error("Party error: Please call load() first before calling new().");
+ }
+
+ return Contract.Pudding.new.apply(Contract, arguments);
+ };
+
+ Contract.at = function() {
+ if (Contract.Pudding == null) {
+ throw new Error("Party error: lease call load() first before calling at().");
+ }
+
+ return Contract.Pudding.at.apply(Contract, arguments);
+ };
+
+ Contract.deployed = function() {
+ if (Contract.Pudding == null) {
+ throw new Error("Party error: Please call load() first before calling deployed().");
+ }
+
+ return Contract.Pudding.deployed.apply(Contract, arguments);
+ };
+
+ if (typeof module != "undefined" && typeof module.exports != "undefined") {
+ module.exports = Contract;
+ } else {
+ // There will only be one version of Pudding in the browser,
+ // and we can use that.
+ window.Party = Contract;
+ }
+
+})();
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..9930d93
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,65 @@
+var gulp = require('gulp');
+var gulpUtil = require('gulp-util');
+var gulpIf = require('gulp-if');
+var concat = require('gulp-concat');
+var uglify = require('gulp-uglify');
+var sass = require('gulp-sass');
+var bowerFiles = require('main-bower-files');
+var ngAnnotate = require('gulp-ng-annotate');
+var ngFileSort = require('gulp-angular-filesort');
+var sourceMaps = require('gulp-sourcemaps');
+var plumber = require('gulp-plumber');
+var streamqueue = require('streamqueue');
+var minifyCss = require('gulp-minify-css');
+
+var config = {
+ paths: {
+ build: "public/",
+ js: ['app/**/*.js'],
+ sass: ['app/**/*.scss'],
+ css: ['app/**/*.css',
+ "bower_components/angular-chart.js/dist/angular-chart.css",
+ "bower_components/angularjs-slider/dist/rzslider.css",
+ "bower_components/sweetalert/dist/sweetalert.css"]
+ },
+ isProduction: gulpUtil.env.production
+};
+
+gulp.task('default', ['watch', 'js', 'css']);
+
+gulp.task('watch', function() {
+ gulp.watch(config.paths.js, ['js']);
+ gulp.watch(config.paths.sass, ['css']);
+});
+
+gulp.task('js', function() {
+ return gulp.src(config.paths.js)
+ .pipe(gulpIf(!config.isProduction, plumber()))
+ .pipe(gulpIf(!config.isProduction, sourceMaps.init()))
+ .pipe(ngFileSort())
+ .pipe(ngAnnotate())
+ .pipe(concat('app.min.js'))
+ .pipe(uglify())
+ .pipe(gulpIf(!config.isProduction, sourceMaps.write()))
+ .pipe(gulp.dest(config.paths.build));
+});
+
+gulp.task('css', function() {
+ return streamqueue({ objectMode: true },
+ gulp.src(config.paths.css),
+ gulp.src(config.paths.sass)
+ .pipe(sass().on('error', sass.logError))
+ )
+ .pipe(concat('styles.min.css'))
+ .pipe(minifyCss())
+ .pipe(gulp.dest(config.paths.build));
+});
+
+gulp.task('vendorjs', function() {
+ return gulp.src(bowerFiles('/**/*.js'))
+ .pipe(gulpIf(!config.isProduction, sourceMaps.init()))
+ .pipe(concat('vendor.min.js'))
+ .pipe(uglify())
+ .pipe(gulpIf(!config.isProduction, sourceMaps.write()))
+ .pipe(gulp.dest(config.paths.build));
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..52843f8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "kpcs",
+ "private": true,
+ "version": "0.0.0",
+ "description": "A implementation of The Kimberley Process Certification System on Ethereum",
+ "license": "MIT",
+ "devDependencies": {
+ "bower": "^1.7.7"
+ },
+ "scripts": {
+ "postinstall": "bower install",
+ "prestart": "npm install",
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "compression": "^1.6.1",
+ "express": "^4.13.4"
+ }
+}
diff --git a/public/assets/ethereumLogo.png b/public/assets/ethereumLogo.png
new file mode 100644
index 0000000..24e1975
Binary files /dev/null and b/public/assets/ethereumLogo.png differ
diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
new file mode 100644
index 0000000..fa559a8
Binary files /dev/null and b/public/favicon-16x16.png differ
diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
new file mode 100644
index 0000000..7fc84b0
Binary files /dev/null and b/public/favicon-32x32.png differ
diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png
new file mode 100644
index 0000000..e784780
Binary files /dev/null and b/public/favicon-96x96.png differ
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..bf8d1f3
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..aefb239
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,222 @@
+
+
+
+ Ether Wheel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ether wheel.io
+
+
+
+ Each time an Ether Wheel is filled, all of the ether* in the wheel will be awarded to a randomly-selected participant. The more ether you contribute, the higher your odds of being the winner. For example, contributing 0.2 ether to the 1-ether wheel would give you a 20% chance of winning it all.
+
+ You currently have Ξ {{ vm.selectedWheel.getContribution(vm.selectedAccount) | number:2 }} in this wheel and Ξ {{ vm.getAccountBalance(vm.selectedAccount).toString() | number:2 }} in your account.
+
+
+
+
+
+
+
+
+
+
+
+
+ This would give you a {{ vm.selectedWheel.desiredContribution / vm.selectedWheel.goal * 100 | number:0 }}% chance of winning all Ξ {{ vm.selectedWheel.goal }}*.
+
+
+
You can also set your contribution by sending ether directly to a wheel with your preferred Ethereum client. This wheel's address is {{ vm.selectedWheel.address }} ( view verified source code).
+
+
+
+
+
+
+
+
+ Recent Winners
+
+
+
This wheel is still on its first round. You could be the first winner!
+ There was a problem connecting to the Ethereum network. Your Ethereum client needs to accept cross-origin requests from this domain by using the following command:
+
+ To put any ether into the wheel, you'll also need to make sure you unlock the account that you want to transfer funds with (e.g. using the --unlock command in geth). If you prefer not unlocking an account, we also provide instructions for sending ether directly to a source code-verified ethereum address.
+
+
+
+ When you're ready, just refresh the page to begin.
+
+
+
+
+
+ *1% of the total winnings are credited to the developer.
+
+
+
+
+
+
+ How do I use this application?
+
+ Ether Wheel is a distributed application running on the Ethereum network. Each wheel has a designated capacity. Users can put as much ether as they want into the wheel. When the wheel is filled up, a winner is randomly selected, weighted by the amount of ether each participant contributed. All of the ether in the wheel is then sent to that account. In other words, the amount of ether that you put in is directly correlated with your chances of winning the entire pot.
+
+
+
+
+ How can I trust that this site works the way it claims?
+
+ You can read the code for yourself! The smart-contract used by Ether Wheel is open source and verified on Ether.camp. This website is just a front-end for that smart-contract. If you don't feel safe unlocking your account for this website, you can still interact with the smart-contract directly by sending ether to its account address using your preferred Ethereum client. See the 'Your Contribution' form above for more details.
+
+
+
+
+ Can my ether be refunded?
+
+ Yes! Before the wheel fills up and a winner is chosen, you can raise or lower your contribution at any time. However, once the wheel has been filled, the ether will be sent to the winner, and this cannot be undone.
+
+
+
+
+ Is it possible to cheat?
+ The short answer: no.
+
+ The long answer: those of you who are cryptographically-savvy may notice that Ether Wheel uses a block hash as the seed to semi-randomly determine the winner. Mathematically-speaking, there does exist a slim chance for a miner using this dapp to abandon a block that does not declare them as the winner. However, since the reward for mining a block is 5 ether, and all ether wheels have a capacity of 5 ether or less, there is absolutely no incentive for a miner to attempt performing this cheat, since it would result in a net loss of ether for them.
+
+
+
+
+ Do you benefit from this?
+
+ The Ether Wheel smart-contract does send me (the developer) 1% of the pool at the time that a winner is declared, in order to compensate me for hosting/domain name fees and my development time. Other than that, Ether Wheel only benefits its users. Keep in mind that ether is not a currency and should not be treated as such. The Ethereum Project defines ether as "an internal token that is used to pay for computation time."
+
+
+
+
+ How can I support this site?
+
+ If you're a developer, feel free to make suggestions or pull requests on GitHub to improve this dapp. If you have feedback or questions, you can reach me on reddit at /u/doppio If you just want to show your approval and support the Ethereum community, why not a few finney to my Ethereum account? 0xEeD1614CDEfBBdAd2cC8Af7AF2fd4beB93F1cCAc
+