├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── components │ ├── LotteryList │ │ ├── lotteryList.scss │ │ ├── lotterylist.reducer.js │ │ ├── lotterylist.saga.js │ │ ├── lotterylist.actions.js │ │ └── index.js │ ├── ReduxedTextField │ │ └── index.js │ ├── UserMessage │ │ ├── user-message.action.js │ │ └── user-message.reducer.js │ ├── LotteryDetails │ │ ├── LotteryDetails.css │ │ ├── LotteryDetails.scss │ │ └── index.js │ ├── Participate │ │ ├── participate.scss │ │ ├── participate.saga.js │ │ ├── participate.actions.js │ │ ├── participate.reducer.js │ │ └── index.js │ ├── NewLottery │ │ ├── newLottery.scss │ │ ├── newlottery.actions.js │ │ ├── newlottery.reducer.js │ │ ├── newlottery.saga.js │ │ └── index.js │ ├── HowToPlay │ │ ├── howToPlay.scss │ │ └── index.js │ ├── Dashboard │ │ ├── dashboard.scss │ │ └── index.js │ └── Lottery │ │ ├── lottery.scss │ │ └── index.js ├── App │ ├── app.actions.js │ ├── app.saga.js │ ├── app.scss │ └── index.js ├── routes.js ├── index.js ├── logo.svg ├── registerServiceWorker.js └── raffle │ └── raffle.js ├── .gitignore ├── lib └── lottery │ ├── test │ ├── cow.js │ ├── index.html │ ├── cow_test.js │ └── lottery.test.js │ ├── contracts │ └── Raffle.sol │ └── raffle.js ├── config └── localhost.config.yaml ├── scripts └── create-metadata.js ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blockapps/charity-raffle/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/LotteryList/lotteryList.scss: -------------------------------------------------------------------------------- 1 | label.md-selection-control-label.md-pointer--hover.md-text { 2 | color: white; 3 | } 4 | 5 | .md-switch-container { 6 | float: right; 7 | } -------------------------------------------------------------------------------- /src/App/app.actions.js: -------------------------------------------------------------------------------- 1 | export const APP_INIT_COMPILE_CONTRACT = 'APP_INIT_COMPILE_CONTRACT'; 2 | 3 | export const appInitCompileContract = function() { 4 | return { 5 | type: APP_INIT_COMPILE_CONTRACT 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | .psci_modules 3 | bower_components 4 | node_modules 5 | 6 | # Generated files 7 | .psci 8 | output 9 | .babelrc 10 | *.log 11 | **/build 12 | **/package 13 | **/.DS_Store 14 | 15 | # css 16 | src/**/*.css 17 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | 4 | import Dashboard from './components/Dashboard/'; 5 | 6 | export const routes = ( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/ReduxedTextField/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from 'react-md/lib/TextFields'; 3 | 4 | const ReduxedTextField = ({ input, meta: { touched, error }, ...others }) => ( 5 | 6 | ); 7 | 8 | export default ReduxedTextField; 9 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /lib/lottery/test/cow.js: -------------------------------------------------------------------------------- 1 | // cow.js 2 | (function(exports) { 3 | "use strict"; 4 | 5 | function Cow(name) { 6 | this.name = name || "Anon cow"; 7 | } 8 | exports.Cow = Cow; 9 | 10 | Cow.prototype = { 11 | greets: function(target) { 12 | if (!target) 13 | throw new Error("missing target"); 14 | return this.name + " greets " + target; 15 | } 16 | }; 17 | 18 | })(this); 19 | -------------------------------------------------------------------------------- /src/components/UserMessage/user-message.action.js: -------------------------------------------------------------------------------- 1 | export const RESET_USER_MESSAGE = 'RESET_USER_MESSAGE'; 2 | export const SET_USER_MESSAGE = 'SET_USER_MESSAGE'; 3 | 4 | // Resets the currently visible user message. 5 | export const resetUserMessage = () => ({ 6 | type: RESET_USER_MESSAGE 7 | }); 8 | 9 | export const setUserMessage = (message) => ({ 10 | type: SET_USER_MESSAGE, 11 | message: message 12 | }); 13 | -------------------------------------------------------------------------------- /config/localhost.config.yaml: -------------------------------------------------------------------------------- 1 | apiDebug: false 2 | password: '1234' 3 | timeout: 600000 4 | contractsPath: ./contracts/ 5 | 6 | # WARNING - extra strict syntax 7 | # DO NOT change the nodes order 8 | # node 0 is the default url for all single node api calls 9 | nodes: 10 | - id: 0 11 | explorerUrl: 'http://localhost:9000' 12 | stratoUrl: 'http://localhost/strato-api' 13 | blocUrl: 'http://localhost/bloc/v2.2' 14 | searchUrl: 'http://localhost/cirrus' 15 | -------------------------------------------------------------------------------- /src/components/LotteryDetails/LotteryDetails.css: -------------------------------------------------------------------------------- 1 | #lottery-detail-dialog-title, #lottery-detail-dialog button { 2 | background: #AC1049; 3 | color: white; } 4 | 5 | #lottery-detail-dialog section { 6 | padding-bottom: 0; } 7 | 8 | .lottery-detail { 9 | margin-top: 24px; } 10 | 11 | .footer-lottery-detail { 12 | justify-content: center; } 13 | 14 | @media (max-width: 992px) { 15 | #lottery-detail-dialog { 16 | max-width: 100%; 17 | width: 90% !important; } } 18 | 19 | @media (max-width: 767px) { 20 | #lottery-detail-dialog { 21 | max-width: 100%; 22 | width: 95% !important; } } 23 | -------------------------------------------------------------------------------- /src/components/UserMessage/user-message.reducer.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './user-message.action'; 2 | 3 | // Updates error message to notify about the failed fetches OR user notification message 4 | const userMessageReducer = function(state = null, action) { 5 | const { type, error } = action; 6 | 7 | if (type === ActionTypes.RESET_USER_MESSAGE) { 8 | return null; 9 | } 10 | else if (error) { 11 | return action.error.message; 12 | } 13 | else if (type === ActionTypes.SET_USER_MESSAGE) { 14 | return action.message; 15 | } 16 | return state; 17 | }; 18 | 19 | export default userMessageReducer; 20 | -------------------------------------------------------------------------------- /scripts/create-metadata.js: -------------------------------------------------------------------------------- 1 | const readJson = require('read-package-json'); 2 | const fs = require('fs'); 3 | 4 | readJson('package.json', (err, data) => { 5 | if (err) { 6 | console.log('Could not read package.json', err); 7 | return; 8 | } 9 | 10 | const metadata = { 11 | name: data.name, 12 | description: data.description, 13 | maintainer: data.author.name, 14 | version: data.version, 15 | }; 16 | 17 | fs.writeFile('package/metadata.json',JSON.stringify(metadata,null,2), 'utf8', (err) => { 18 | if(err) { 19 | console.log('Failed to write metadata.json', err); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/App/app.saga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeLatest, 3 | call 4 | } from 'redux-saga/effects'; 5 | import { 6 | APP_INIT_COMPILE_CONTRACT 7 | } from './app.actions'; 8 | 9 | import { compileSearch, isCompiled } from '../raffle/raffle'; 10 | 11 | function* compileLotteryContract(action) { 12 | try { 13 | const contractCompiled = yield call(isCompiled); 14 | if(!contractCompiled) { 15 | yield call(compileSearch); 16 | } 17 | } 18 | catch (err) { 19 | // dont do anything 20 | } 21 | } 22 | 23 | export function* watchAppInit() { 24 | yield takeLatest(APP_INIT_COMPILE_CONTRACT, compileLotteryContract); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/LotteryDetails/LotteryDetails.scss: -------------------------------------------------------------------------------- 1 | #lottery-detail-dialog-title, #lottery-detail-dialog button { 2 | background: #AC1049; 3 | color: white; 4 | } 5 | 6 | #lottery-detail-dialog section { 7 | padding-bottom: 0; 8 | } 9 | 10 | .lottery-detail { 11 | margin-top: 24px; 12 | } 13 | 14 | .footer-lottery-detail { 15 | justify-content: center 16 | } 17 | 18 | @media (max-width: 992px) { 19 | #lottery-detail-dialog { 20 | max-width:100%; 21 | width: 90% !important; 22 | } 23 | } 24 | 25 | @media (max-width: 767px) { 26 | #lottery-detail-dialog { 27 | max-width: 100%; 28 | width: 95% !important; 29 | } 30 | } -------------------------------------------------------------------------------- /src/App/app.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/react-md/src/scss/react-md'; 2 | @import '../../node_modules/react-md/dist/react-md.indigo-pink.min.css'; 3 | 4 | $md-primary-color: $md-blue-grey-900; 5 | $md-secondary-color: #AC1049; 6 | 7 | .md-btn--toolbar { 8 | display: flex; 9 | align-items: center; 10 | } 11 | 12 | body { 13 | background: #37474f !important; 14 | margin: 0; 15 | } 16 | 17 | html { 18 | background: #37474f !important; 19 | height: 100%; 20 | } 21 | 22 | .md-dialog--centered { 23 | overflow: auto; 24 | } 25 | 26 | .md-dialog-content { 27 | overflow: auto; 28 | max-height: calc(100% - 72px - 52px); 29 | } 30 | 31 | @include react-md-everything; -------------------------------------------------------------------------------- /src/components/Participate/participate.scss: -------------------------------------------------------------------------------- 1 | #participate-raffle-title, #participate-raffle button { 2 | background: #AC1049; 3 | color: white; 4 | &.disabled { 5 | background: #999; 6 | } 7 | } 8 | 9 | .footer-new-participate { 10 | justify-content: center 11 | } 12 | 13 | #participate-raffle .md-password-btn { 14 | margin-top:-10px; 15 | width: 30px; 16 | margin-top: -13px; 17 | } 18 | 19 | .label-form { 20 | margin: 0px 10px 0px 0px; 21 | } 22 | 23 | .md-text-field { 24 | margin: 0px; 25 | } 26 | 27 | .lottery-participate { 28 | margin-top: 24px; 29 | } 30 | 31 | @media (max-width: 992px) { 32 | #participate-raffle { 33 | max-width:100%; 34 | width: 90% !important; 35 | } 36 | } 37 | 38 | @media (max-width: 767px) { 39 | #participate-raffle { 40 | max-width: 100%; 41 | width: 95% !important; 42 | } 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/lottery/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The HTML5 Herald 8 | 9 | 10 | 11 | 12 | 13 |

Index

14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/LotteryList/lotterylist.reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOTTERY_LIST_SUCCESS, 3 | TOGGLE_COMPLETED_RAFFELS, 4 | TOGGLE_INPROGRESS_RAFFELS 5 | } from './lotterylist.actions'; 6 | 7 | const initialState = { 8 | lotteries: [], 9 | displayCompleted: true, 10 | displayInProgress: true, 11 | }; 12 | 13 | 14 | const reducer = function (state = initialState, action) { 15 | switch (action.type) { 16 | case LOTTERY_LIST_SUCCESS: 17 | return { 18 | ...state, 19 | lotteries: action.lotteries, 20 | } 21 | case TOGGLE_COMPLETED_RAFFELS: 22 | return { 23 | ...state, 24 | displayCompleted: !state.displayCompleted 25 | } 26 | case TOGGLE_INPROGRESS_RAFFELS: 27 | return { 28 | ...state, 29 | displayInProgress: !state.displayInProgress 30 | } 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | export default reducer; 37 | -------------------------------------------------------------------------------- /lib/lottery/test/cow_test.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; 2 | 3 | describe("Cow", function() { 4 | describe("constructor", function() { 5 | it("should have a default name", function*() { 6 | var cow = new Cow(); 7 | const a = yield fetch('https://google.com'); 8 | expect(cow.name).to.equal("Anon cow"); 9 | }); 10 | 11 | it("should set cow's name if provided", function() { 12 | var cow = new Cow("Kate"); 13 | expect(cow.name).to.equal("Kate"); 14 | }); 15 | }); 16 | 17 | describe("#greets", function() { 18 | it("should throw if no target is passed in", function() { 19 | expect(function() { 20 | (new Cow()).greets(); 21 | }).to.throw(Error); 22 | }); 23 | 24 | it("should greet passed target", function() { 25 | var greetings = (new Cow("Kate")).greets("Baby"); 26 | expect(greetings).to.equal("Kate greets Baby"); 27 | }); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/NewLottery/newLottery.scss: -------------------------------------------------------------------------------- 1 | .open-modal { 2 | background: #AC1049; 3 | margin: 0 !important; 4 | } 5 | 6 | #simple-new-raffle-title, #simple-new-raffle button { 7 | background: #AC1049; 8 | color: white; 9 | &.disabled { 10 | background: #999; 11 | } 12 | } 13 | 14 | #simple-new-raffle section { 15 | color: black; 16 | } 17 | 18 | .footer-new-raffle { 19 | justify-content: center 20 | } 21 | 22 | .md-text { 23 | margin:0px; 24 | } 25 | 26 | .md-text-field-container--input { 27 | margin: 0px; 28 | } 29 | 30 | .new-lottery-modal .md-password-btn { 31 | margin-top:-10px; 32 | width: 30px; 33 | margin-top: -13px; 34 | } 35 | 36 | @media (max-width: 992px) { 37 | #simple-new-raffle { 38 | max-width:100%; 39 | width: 90% !important; 40 | } 41 | } 42 | 43 | @media (max-width: 767px) { 44 | #simple-new-raffle { 45 | max-width:100%; 46 | width: 95% !important; 47 | } 48 | } -------------------------------------------------------------------------------- /src/components/LotteryList/lotterylist.saga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeEvery, 3 | put, 4 | call 5 | } from 'redux-saga/effects'; 6 | import { 7 | LOTTERY_LIST_REQUEST, 8 | lotteryListSuccess, 9 | lotteryListFailure, 10 | } from './lotterylist.actions'; 11 | 12 | import {getOpen} from '../../raffle/raffle' 13 | 14 | //const addressZero = '0000000000000000000000000000000000000000'; 15 | //const results = yield rest.query(`${contractName}?winnerAddress=eq.${addressZero}`); 16 | 17 | function* lotteryListAPICall() { 18 | return yield call(getOpen); 19 | } 20 | 21 | function* makeLotteryListRequest(action) { 22 | try { 23 | const lotteries = yield lotteryListAPICall() 24 | yield put(lotteryListSuccess(lotteries)); 25 | } 26 | catch(err) { 27 | yield put(lotteryListFailure(err)); 28 | } 29 | } 30 | 31 | export function* watchLotteryList() { 32 | yield takeEvery(LOTTERY_LIST_REQUEST, makeLotteryListRequest); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/HowToPlay/howToPlay.scss: -------------------------------------------------------------------------------- 1 | .ht-play-section { 2 | margin-bottom: 0; 3 | } 4 | 5 | 6 | .ht-play .ht-play-content { 7 | margin-top: 24px; 8 | } 9 | 10 | .ht-play #simple-action-dialog-title{ 11 | background: #AC1049; 12 | color: white; 13 | } 14 | 15 | .htp-button button { 16 | background: #AC1049; 17 | color: white; 18 | } 19 | 20 | .ht-play-content p { 21 | padding-bottom: 10px; 22 | } 23 | 24 | #simple-action-dialog section { 25 | padding-bottom: 0; 26 | } 27 | 28 | #simple-action-dialog button { 29 | background: #AC1049; 30 | color: white; 31 | } 32 | 33 | .ht-play-section button { 34 | margin:0px; 35 | } 36 | 37 | .footer-ht-play { 38 | justify-content: center 39 | } 40 | 41 | @media (max-width: 992px) { 42 | #simple-action-dialog { 43 | max-width:100%; 44 | width: 90% !important; 45 | } 46 | } 47 | 48 | @media (max-width: 767px) { 49 | #simple-action-dialog { 50 | max-width: 100%; 51 | width: 95% !important; 52 | } 53 | } -------------------------------------------------------------------------------- /src/components/Dashboard/dashboard.scss: -------------------------------------------------------------------------------- 1 | .ht-play-section { 2 | float: right; 3 | display: table; 4 | } 5 | 6 | .new-lottery-modal { 7 | float: right; 8 | display: table; 9 | margin-right: 10px; 10 | } 11 | 12 | .footer-ht-play { 13 | justify-content: center; 14 | } 15 | 16 | .lottery-buttons button { 17 | color: white; 18 | margin-right: 8px; 19 | } 20 | 21 | .lbutton-highlighted { 22 | background: #AC1049; 23 | } 24 | 25 | @media (max-width: 767px) { 26 | .ht-play-section { 27 | width: 100%; 28 | padding: 0px 15px; 29 | } 30 | .ht-play-open { 31 | width: 100% !important; 32 | } 33 | .new-lottery-modal { 34 | width: 100%; 35 | padding: 0px 15px; 36 | margin: 0px; 37 | margin-top: 15px; 38 | } 39 | .new-lottery-modal button { 40 | width: 100%; 41 | } 42 | .lottery-buttons { 43 | width: 100%; 44 | padding: 0px 15px; 45 | margin: 0px; 46 | margin-top: 15px; 47 | } 48 | .lottery-buttons .in-progress-btn { 49 | margin-top: 30px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/LotteryList/lotterylist.actions.js: -------------------------------------------------------------------------------- 1 | export const LOTTERY_LIST_REQUEST = 'LOTTERY_LIST_REQUEST'; 2 | export const LOTTERY_LIST_SUCCESS = 'LOTTERY_LIST_SUCCESS'; 3 | export const LOTTERY_LIST_FAILURE = 'LOTTERY_LIST_FAILURE'; 4 | export const TOGGLE_COMPLETED_RAFFELS = 'TOGGLE_COMPLETED_RAFFELS'; 5 | export const TOGGLE_INPROGRESS_RAFFELS = 'TOGGLE_INPROGRESS_RAFFELS'; 6 | 7 | export const lotteryListRequest = function() { 8 | return { 9 | type: LOTTERY_LIST_REQUEST 10 | }; 11 | } 12 | 13 | export const lotteryListSuccess = function(result) { 14 | return { 15 | type: LOTTERY_LIST_SUCCESS, 16 | lotteries: result, 17 | }; 18 | } 19 | 20 | export const lotteryListFailure = function(error) { 21 | return { 22 | type: LOTTERY_LIST_FAILURE, 23 | result: error 24 | }; 25 | } 26 | 27 | export const toggleCompletedRaffles = function() { 28 | return { 29 | type: TOGGLE_COMPLETED_RAFFELS 30 | } 31 | } 32 | 33 | export const toggleInProgressRaffles = function() { 34 | return { 35 | type: TOGGLE_INPROGRESS_RAFFELS 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/Participate/participate.saga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeEvery, 3 | put, 4 | call 5 | } from 'redux-saga/effects'; 6 | import { 7 | PARTICIPATE_REQUEST, 8 | participateSuccess, 9 | participateFailure, 10 | } from './participate.actions'; 11 | 12 | import { enter } from '../../raffle/raffle'; 13 | import { setUserMessage } from '../UserMessage/user-message.action' 14 | 15 | function* participateAPICall(payload) { 16 | return yield call(enter, payload); 17 | } 18 | 19 | function* makeParticipateRequest(action) { 20 | try 21 | { 22 | const response = yield call(participateAPICall,action.payload); 23 | yield put(participateSuccess(action.key, response)); 24 | yield put(setUserMessage('Successfully acquired the tickets')); 25 | } 26 | catch(err) { 27 | yield put(participateFailure(action.key, err)); 28 | //yield put(setUserMessage('Error: Something went wrong')); 29 | } 30 | } 31 | 32 | export function* watchParticipate() { 33 | yield takeEvery(PARTICIPATE_REQUEST, makeParticipateRequest); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/NewLottery/newlottery.actions.js: -------------------------------------------------------------------------------- 1 | export const NEW_LOTTERY_OPEN_MODAL = 'NEW_LOTTERY_OPEN_MODAL'; 2 | export const NEW_LOTTERY_CLOSE_MODAL = 'NEW_LOTTERY_CLOSE_MODAL'; 3 | export const NEW_LOTTERY_REQUEST = 'NEW_LOTTERY_REQUEST'; 4 | export const NEW_LOTTERY_SUCCESS = 'NEW_LOTTERY_SUCCESS'; 5 | export const NEW_LOTTERY_FAILURE = 'NEW_LOTTERY_FAILURE'; 6 | 7 | export const newLotteryOpenModal = function() { 8 | return { 9 | type: NEW_LOTTERY_OPEN_MODAL, 10 | }; 11 | } 12 | 13 | export const newLotteryCloseModal = function() { 14 | return { 15 | type: NEW_LOTTERY_CLOSE_MODAL, 16 | }; 17 | } 18 | 19 | export const newLotteryCall = function(payload) { 20 | return { 21 | type: NEW_LOTTERY_REQUEST, 22 | payload: payload, 23 | submitting: true, 24 | }; 25 | } 26 | 27 | export const newLotterySuccess = function(result) { 28 | return { 29 | type: NEW_LOTTERY_SUCCESS, 30 | result: result, 31 | submitting: false, 32 | }; 33 | } 34 | 35 | export const newLotteryFailure = function(error) { 36 | return { 37 | type: NEW_LOTTERY_FAILURE, 38 | result: error, 39 | submitting: false, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/NewLottery/newlottery.reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_LOTTERY_CLOSE_MODAL, 3 | NEW_LOTTERY_OPEN_MODAL, 4 | NEW_LOTTERY_REQUEST, 5 | NEW_LOTTERY_SUCCESS, 6 | NEW_LOTTERY_FAILURE 7 | } from './newlottery.actions'; 8 | 9 | const initialState = { 10 | isOpen: false, 11 | failure: null 12 | }; 13 | 14 | 15 | const reducer = function (state = initialState, action) { 16 | switch (action.type) { 17 | case NEW_LOTTERY_OPEN_MODAL: 18 | return { 19 | isOpen: true, 20 | } 21 | case NEW_LOTTERY_CLOSE_MODAL: 22 | return { 23 | isOpen: false, 24 | } 25 | case NEW_LOTTERY_REQUEST: 26 | return { 27 | // TODO: Add a loading icon until success or failure 28 | isOpen: true, 29 | } 30 | case NEW_LOTTERY_SUCCESS: 31 | return { 32 | isOpen: false, 33 | failure: null 34 | } 35 | case NEW_LOTTERY_FAILURE: 36 | return { 37 | isOpen: true, 38 | failure: action.result, 39 | } 40 | default: 41 | return state; 42 | } 43 | } 44 | 45 | export default reducer; 46 | 47 | -------------------------------------------------------------------------------- /src/components/NewLottery/newlottery.saga.js: -------------------------------------------------------------------------------- 1 | import { 2 | takeEvery, 3 | put, 4 | call 5 | } from 'redux-saga/effects'; 6 | import { 7 | NEW_LOTTERY_REQUEST, 8 | newLotterySuccess, 9 | newLotteryFailure, 10 | } from './newlottery.actions'; 11 | 12 | import { uploadContract } from '../../raffle/raffle'; 13 | import { setUserMessage } from '../UserMessage/user-message.action' 14 | import { lotteryListRequest } from '../LotteryList/lotterylist.actions'; 15 | 16 | //const compileUrl = env.STRATO_URL + "/extabi"; 17 | //const blocCompileUrl = env.BLOC_URL + "/contracts/compile"; 18 | 19 | function* newLotteryAPICall(payload) { 20 | return yield call(uploadContract,payload); 21 | } 22 | 23 | function* makeNewLotteryRequest(action) { 24 | try { 25 | const response = yield newLotteryAPICall(action.payload); 26 | yield put(newLotterySuccess(response)); 27 | yield put(lotteryListRequest()); 28 | yield put(setUserMessage('Successfully created the raffle')); 29 | } 30 | catch(err) { 31 | yield put(newLotteryFailure(err.message)); 32 | //yield put(setUserMessage('Error: Something went wrong')); 33 | } 34 | } 35 | 36 | export function* watchNewLottery() { 37 | yield takeEvery(NEW_LOTTERY_REQUEST, makeNewLotteryRequest); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Participate/participate.actions.js: -------------------------------------------------------------------------------- 1 | export const PARTICIPATE_OPEN_MODAL = 'PARTICIPATE_OPEN_MODAL'; 2 | export const PARTICIPATE_CLOSE_MODAL = 'PARTICIPATE_CLOSE_MODAL'; 3 | export const PARTICIPATE_REQUEST = 'PARTICIPATE_REQUEST'; 4 | export const PARTICIPATE_SUCCESS = 'PARTICIPATE_SUCCESS'; 5 | export const PARTICIPATE_FAILURE = 'PARTICIPATE_FAILURE'; 6 | 7 | export const participateOpenModal = function(key) { 8 | return { 9 | type: PARTICIPATE_OPEN_MODAL, 10 | key: key, 11 | }; 12 | } 13 | 14 | export const participateCloseModal = function(key) { 15 | return { 16 | type: PARTICIPATE_CLOSE_MODAL, 17 | key: key, 18 | }; 19 | } 20 | 21 | export const participateRequest = function(key,payload) { 22 | return { 23 | type: PARTICIPATE_REQUEST, 24 | payload: payload, 25 | key: key, 26 | submitting: true, 27 | }; 28 | } 29 | 30 | export const participateSuccess = function(key,result) { 31 | return { 32 | type: PARTICIPATE_SUCCESS, 33 | result: result, 34 | key: key, 35 | submitting: false, 36 | }; 37 | } 38 | 39 | export const participateFailure = function(key,error) { 40 | return { 41 | type: PARTICIPATE_FAILURE, 42 | result: error, 43 | key: key, 44 | submitting: false, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "charity-raffle", 3 | "description": "This apps demonstrates how to create a DApp using the STRATO platform", 4 | "version": "0.1.10", 5 | "author": "BlockApps Inc", 6 | "engines": { 7 | "npm": "v5.6.0", 8 | "node": "v8.9.4" 9 | }, 10 | "homepage": ".", 11 | "private": true, 12 | "dependencies": { 13 | "bootstrap": "^3.3.7", 14 | "react": "16.2.0", 15 | "react-dom": "16.2.0", 16 | "react-md": "^1.2.11", 17 | "react-redux": "5.0.6", 18 | "react-redux-loading-bar": "^3.1.1", 19 | "react-router-dom": "4.2.2", 20 | "react-router-redux": "4.0.8", 21 | "react-scripts": "1.0.14", 22 | "read-package-json": "^2.0.12", 23 | "redux": "^3.7.2", 24 | "redux-form": "^7.1.1", 25 | "redux-saga": "^0.16.0", 26 | "reselect": "^3.0.1" 27 | }, 28 | "scripts": { 29 | "start": "npm-run-all -p watch-css start-js", 30 | "start-js": "react-scripts start", 31 | "build": "npm run build-css && react-scripts build && mkdir package && mkdir package/contracts && node scripts/create-metadata.js && mv build/* package/ && cd package && zip -r app.zip * && cd .. && rm -rf build && mv package build", 32 | "test": "react-scripts test --env=jsdom", 33 | "eject": "react-scripts eject", 34 | "build-css": "node-sass-chokidar src/ -o src/", 35 | "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive" 36 | }, 37 | "devDependencies": { 38 | "node-sass-chokidar": "0.0.3", 39 | "npm-run-all": "^4.1.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Lottery/lottery.scss: -------------------------------------------------------------------------------- 1 | .lottery { 2 | width: calc(100% / 4 - 10px ); 3 | float: left; 4 | margin: 10px 5px; 5 | border-radius: 6px; 6 | box-shadow: 1px 2px 1px 4px #333; 7 | 8 | } 9 | 10 | .lottery-title { 11 | display: block; 12 | background: #949393; 13 | text-align: center; 14 | border-radius: 0px; 15 | cursor: pointer; 16 | padding:0px; 17 | } 18 | 19 | .lottery-title h2 { 20 | color: white; 21 | font-size: 18px; 22 | padding: 5px 0px; 23 | } 24 | 25 | .lottery-button { 26 | background: #AC1049; 27 | color: white; 28 | margin:0px auto; 29 | display: table; 30 | } 31 | 32 | .lottery-disabled { 33 | background: #D7D6D6; 34 | } 35 | 36 | .md-card-text.card-text:last-child{padding-bottom: 10px;} 37 | 38 | .card-text p{ 39 | font-size: inherit; 40 | border-bottom: 1px solid #999; 41 | line-height: 30px; 42 | } 43 | 44 | .card-text b{font-weight: normal;} 45 | .lottery-disabled { 46 | // text-overflow: ellipsis; 47 | // width: 100%; 48 | // overflow: hidden; 49 | background: green; 50 | margin: 0px auto; 51 | display: table; 52 | color: white; 53 | } 54 | @media (max-width: 992px) { 55 | .lottery { 56 | width: calc(100% / 3 - 10px); 57 | float: left; 58 | margin: 10px 5px; 59 | } 60 | } 61 | 62 | @media (max-width: 767px) { 63 | .lottery { 64 | width: 48%; 65 | margin: 10px 5px; 66 | border-radius: 6px; 67 | } 68 | .lottery-buttons button{ 69 | width: 100%; 70 | } 71 | } 72 | 73 | @media (max-width: 550px) { 74 | .lottery { 75 | width: 100%; 76 | margin: 10px 0px; 77 | border-radius: 6px; 78 | } 79 | } -------------------------------------------------------------------------------- /src/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import './app.css'; 5 | import { routes as scenes } from '../routes'; 6 | import { Toolbar } from 'react-md'; 7 | import { appInitCompileContract } from './app.actions'; 8 | import LoadingBar from 'react-redux-loading-bar'; 9 | import Snackbar from 'react-md/lib/Snackbars'; 10 | 11 | import { resetUserMessage, setUserMessage } from '../components/UserMessage/user-message.action'; 12 | 13 | class App extends Component { 14 | 15 | componentWillMount() { 16 | this.props.appInitCompileContract(); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | 23 | 27 | { this.props.resetUserMessage() }} /> 34 | {scenes} 35 |
36 | ); 37 | } 38 | } 39 | 40 | function mapStateToProps(state) { 41 | return { 42 | userMessage: state.userMessage, 43 | }; 44 | } 45 | 46 | const connected = connect(mapStateToProps, { 47 | appInitCompileContract, 48 | setUserMessage, 49 | resetUserMessage, 50 | })(App); 51 | 52 | export default withRouter(connected); 53 | -------------------------------------------------------------------------------- /src/components/LotteryDetails/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { DialogContainer} from 'react-md'; 5 | import './LotteryDetails.css'; 6 | 7 | class LotteryDetails extends Component { 8 | 9 | render() { 10 | const actions = [{ 11 | onClick: this.props.handleModal.bind(this, false), 12 | primary: true, 13 | children: 'Close', 14 | }]; 15 | 16 | return ( 26 |
27 |

28 | {this.props.lotteryData.description} 29 |

30 |

Jackpot: {this.props.lotteryData.ticketPrice * this.props.lotteryData.ticketCount} coins

31 |

Ticket remaining: {this.props.lotteryData.ticketCount - this.props.lotteryData.entries.length}

32 |

Price: {this.props.lotteryData.ticketPrice} coins per ticket

33 |
34 |
); 35 | } 36 | } 37 | 38 | const mapStateToProps = (state, props) => { 39 | return { 40 | lotteries: state.lotteryList.lotteries, 41 | address: props.match.params.address 42 | }; 43 | } 44 | 45 | const connected = connect(mapStateToProps)(LotteryDetails); 46 | 47 | export default withRouter(connected); 48 | -------------------------------------------------------------------------------- /src/components/LotteryList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import Lottery from '../Lottery'; 5 | import { lotteryListRequest } from './lotterylist.actions'; 6 | import './lotteryList.css'; 7 | 8 | class LotteryList extends Component { 9 | 10 | componentWillMount() { 11 | this.startPoll(); 12 | this.props.lotteryListRequest(this.props.showAll); 13 | } 14 | 15 | componentWillUnmount() { 16 | this.stopTimer() 17 | } 18 | 19 | stopTimer() { 20 | clearInterval(this.timeout); 21 | } 22 | 23 | startPoll() { 24 | var self = this 25 | this.timeout = setInterval(function () { 26 | self.props.lotteryListRequest() 27 | }, 5 * 1000); 28 | } 29 | 30 | render() { 31 | 32 | const lotteries = Array.isArray(this.props.lotteries) 33 | ? this.props.lotteries 34 | .filter((item) => { 35 | const remaining = item.ticketCount - item.entries.length; 36 | return (this.props.displayCompleted && remaining <= 0) 37 | || (this.props.displayInProgress && remaining > 0) 38 | }) 39 | .map((item, i) => { 40 | return () 41 | }) 42 | : []; 43 | 44 | return ( 45 |
46 |
47 | {lotteries} 48 |
49 |
50 | ); 51 | } 52 | } 53 | 54 | function mapStateToProps(state) { 55 | return { 56 | lotteries: state.lotteryList.lotteries, 57 | displayCompleted: state.lotteryList.displayCompleted, 58 | displayInProgress: state.lotteryList.displayInProgress 59 | }; 60 | } 61 | 62 | const connected = connect( 63 | mapStateToProps, 64 | { lotteryListRequest } 65 | )(LotteryList); 66 | 67 | export default withRouter(connected); 68 | -------------------------------------------------------------------------------- /lib/lottery/contracts/Raffle.sol: -------------------------------------------------------------------------------- 1 | //pragma solidity ^0.4.8; 2 | 3 | contract Raffle { 4 | address[] public entries; 5 | uint public ticketCount; 6 | uint public ticketPrice; 7 | string public name; 8 | 9 | uint public winner; 10 | address public winnerAddress; 11 | 12 | uint public charityPercentage; 13 | address public initiator; 14 | 15 | function Raffle(string _name, uint _ticketCount, uint _ticketPrice, uint _charityPercentage) { 16 | // if ticket count < 2 - whats the point 17 | if (_ticketCount < 2) { 18 | throw; 19 | } 20 | // all good 21 | name = _name; 22 | ticketCount = _ticketCount; 23 | ticketPrice = _ticketPrice; 24 | charityPercentage = _charityPercentage; 25 | winnerAddress = 0; 26 | initiator = msg.sender; 27 | } 28 | 29 | function enter(uint _numTickets) payable returns (bool) { 30 | // check if ticket price satisfied 31 | if (msg.value < ticketPrice * _numTickets) { 32 | return false; 33 | } 34 | // check capacity 35 | if (entries.length > ticketCount - _numTickets) { 36 | return false; 37 | } 38 | // enter the lottery 39 | for(uint i=0; i<_numTickets; i++) { 40 | entries.push(msg.sender); 41 | } 42 | // payout 43 | if (entries.length >= ticketCount) { 44 | return payout(); 45 | } 46 | return true; 47 | } 48 | 49 | /* return a random index into entries */ 50 | function rand(uint seed) internal returns (uint) { 51 | return uint(keccak256(seed)) % entries.length; 52 | } 53 | 54 | function testRand(uint seed) returns (uint) { 55 | if (entries.length < 2) { 56 | return 99999999; 57 | } 58 | return rand(seed); 59 | } 60 | 61 | function payout() internal returns (bool){ 62 | winner = rand(block.number); 63 | winnerAddress = entries[winner]; 64 | uint charityAmount = this.balance * charityPercentage / 100; 65 | winnerAddress.send(this.balance-charityAmount); 66 | initiator.send(charityAmount); 67 | return true; 68 | } 69 | } -------------------------------------------------------------------------------- /src/components/HowToPlay/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, DialogContainer } from 'react-md'; 3 | import './howToPlay.css'; 4 | 5 | class HowToPlay extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | isOpen: false 11 | } 12 | } 13 | 14 | handleModal(isOpen) { 15 | this.setState({ isOpen }); 16 | } 17 | 18 | render() { 19 | const actions = [{ 20 | onClick: this.handleModal.bind(this, false), 21 | primary: true, 22 | children: 'Close', 23 | }]; 24 | 25 | return ( 26 |
27 |
28 | 31 |
32 | 42 |
43 | 46 |

47 | To create your own raffle, click ​Create New Raffle​ and enter your account information and raffle description. 48 |

49 | 50 | 53 |

54 | To participate in an existing raffle, select a raffle, click ​ Play​ and enter your account information for a chance to win some ether. 55 |

56 |
57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | export default HowToPlay; 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | Charity Raffle 28 | 29 | 30 | 33 |
34 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/components/Participate/participate.reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | PARTICIPATE_CLOSE_MODAL, 3 | PARTICIPATE_OPEN_MODAL, 4 | PARTICIPATE_REQUEST, 5 | PARTICIPATE_SUCCESS, 6 | PARTICIPATE_FAILURE 7 | } from './participate.actions'; 8 | 9 | const initialState = { 10 | modals: {} 11 | }; 12 | 13 | 14 | const reducer = function (state = initialState, action) { 15 | switch (action.type) { 16 | case PARTICIPATE_OPEN_MODAL: 17 | return { 18 | modals: { 19 | ...state.modals, 20 | [action.key] : { 21 | ...state.modals[action.key], 22 | isOpen: true, 23 | result: 'Waiting for method to be called...' 24 | } 25 | } 26 | } 27 | case PARTICIPATE_CLOSE_MODAL: 28 | return { 29 | modals: { 30 | ...state.modals, 31 | [action.key] : { 32 | ...state.modals[action.key], 33 | isOpen: false, 34 | result: 'Waiting for method to be called...' 35 | } 36 | } 37 | } 38 | case PARTICIPATE_REQUEST: 39 | return { 40 | modals: { 41 | ...state.modals, 42 | [action.key] : { 43 | ...state.modals[action.key], 44 | isOpen: true, 45 | result: 'Waiting for method to be called...' 46 | } 47 | } 48 | } 49 | case PARTICIPATE_SUCCESS: 50 | return { 51 | modals: { 52 | ...state.modals, 53 | [action.key] : { 54 | ...state.modals[action.key], 55 | isOpen: false, 56 | result: 'Waiting for method to be called...' 57 | } 58 | } 59 | } 60 | case PARTICIPATE_FAILURE: 61 | return { 62 | modals: { 63 | ...state.modals, 64 | [action.key] : { 65 | ...state.modals[action.key], 66 | isOpen: true, 67 | result: 'Waiting for method to be called...' 68 | } 69 | }, 70 | failure: action.result, 71 | } 72 | default: 73 | return state; 74 | } 75 | } 76 | 77 | export default reducer; 78 | 79 | -------------------------------------------------------------------------------- /src/components/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import { Button } from 'react-md'; 4 | import { connect } from 'react-redux'; 5 | 6 | import HowToPlay from '../HowToPlay'; 7 | import LotteryList from '../LotteryList'; 8 | import NewLottery from '../NewLottery'; 9 | import { 10 | toggleCompletedRaffles, toggleInProgressRaffles 11 | } from '../LotteryList/lotterylist.actions'; 12 | import './dashboard.css' 13 | 14 | class Dashboard extends Component { 15 | render() { 16 | return ( 17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 | 32 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | } 55 | 56 | function mapStateToProps(state) { 57 | return { 58 | displayCompleted: state.lotteryList.displayCompleted, 59 | displayInProgress: state.lotteryList.displayInProgress 60 | }; 61 | } 62 | 63 | const connected = connect( 64 | mapStateToProps, 65 | { 66 | toggleCompletedRaffles, 67 | toggleInProgressRaffles 68 | } 69 | )(Dashboard); 70 | 71 | export default withRouter(connected); 72 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {reducer as formReducer} from 'redux-form'; 4 | import {Provider} from 'react-redux'; 5 | import {HashRouter as Router} from 'react-router-dom'; 6 | import { 7 | createStore, 8 | applyMiddleware, 9 | combineReducers, 10 | compose, 11 | } from 'redux'; 12 | import createSagaMiddleware from 'redux-saga'; 13 | import { fork } from 'redux-saga/effects'; 14 | import {routerReducer} from 'react-router-redux'; 15 | import lotteryListReducer from './components/LotteryList/lotterylist.reducer' 16 | import newLotteryReducer from './components/NewLottery/newlottery.reducer' 17 | import participateReducer from './components/Participate/participate.reducer' 18 | 19 | import App from './App/'; 20 | import {unregister as unregisterServiceWorker} from './registerServiceWorker'; 21 | 22 | import { watchLotteryList } from './components/LotteryList/lotterylist.saga'; 23 | import { watchNewLottery } from './components/NewLottery/newlottery.saga'; 24 | import { watchParticipate } from './components/Participate/participate.saga'; 25 | import { watchAppInit } from './App/app.saga'; 26 | 27 | import { loadingBarReducer, loadingBarMiddleware } from 'react-redux-loading-bar'; 28 | import userMessageReducer from './components/UserMessage/user-message.reducer'; 29 | 30 | const rootReducer = combineReducers({ 31 | form: formReducer, 32 | routing: routerReducer, 33 | lotteryList: lotteryListReducer, 34 | newLottery: newLotteryReducer, 35 | participate: participateReducer, 36 | // YOUR REDUCERS HERE 37 | loadingBar: loadingBarReducer, 38 | userMessage: userMessageReducer, 39 | }); 40 | 41 | const rootSaga = function* startForeman() { 42 | yield [ 43 | // YOUR SAGAS HERE 44 | fork(watchLotteryList), 45 | fork(watchNewLottery), 46 | fork(watchParticipate), 47 | fork(watchAppInit) 48 | ]; 49 | }; 50 | 51 | const sagaMiddleware = createSagaMiddleware(); 52 | 53 | const loadingMiddleware = loadingBarMiddleware({ 54 | promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAILURE'] 55 | }); 56 | 57 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 58 | 59 | 60 | 61 | const store = createStore( 62 | rootReducer, 63 | composeEnhancers(applyMiddleware(sagaMiddleware, loadingMiddleware)) 64 | ); 65 | 66 | sagaMiddleware.run(rootSaga); 67 | 68 | ReactDOM.render( 69 | 70 | 71 | 72 | 73 | , 74 | document.getElementById('root') 75 | ); 76 | unregisterServiceWorker(); 77 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Charity Raffle dApp 2 | 3 | This is a demo dApp built to be deployable on the [BlockApps STRATO](http://developers.blockapps.net/) platform. The app consists of a raffle smart contract and a react user interface. Any user can initiate the creation of a raffle contract. The user determines the number of total tickets, the price per ticket and the perentage to donate to a charity of their chosing, when deploying the smart contract. Various users can purchase these tickets. The raffle is closed and a winner is picked once the total number of participants is reached. 4 | 5 | Read the [documentation](https://developers.blockapps.net/advanced/launch-dapp/) for more information on creating dApps using [BlockApps STRATO](http://blockapps.net/blockapps-strato-blockchain-application-development/). 6 | 7 | ## Pre-requisites 8 | A running STRATO multinode network is required to deploy and run this application. Follow the instructions in the [STRATO getting started guide](https://github.com/blockapps/strato-getting-started) to setup a multi-node network. Additional instructions can be found on our [Developer Portal](https://developers.blockapps.net/advanced/multi-node/). 9 | 10 | ## Building 11 | The STRATO platform expects a zip archive with the following structure: 12 | 13 | ``` 14 | .zip 15 | ├─ contracts/ 16 | │ └─ ...all solidity contract files (*.sol) 17 | │ 18 | ├─ index.html 19 | │ 20 | ├─ ...other ui application files used by index.html 21 | │ 22 | └─ metadata.json 23 | ``` 24 | 25 | Running the following commands will generate the directory structure and the zip archive under the `build` folder. 26 | 27 | ``` 28 | npm install 29 | npm run build 30 | ``` 31 | 32 | The data in `metadata.json` is populated from `package.json`. 33 | 34 | This zip archive under `build/app.zip` can be uploaded directly to STRATO. 35 | 36 | ## Deploying on STRATO 37 | Perform the following steps to deploy this demo app on STRATO. 38 | 1. Access the STRATO dashboard (See [STRATO getting started](https://github.com/blockapps/strato-getting-started)) 39 | 2. Create and faucet and account (a private key) to upload and sign the deployment, using the accounts tab in the STRATO management dashboard. This is only necessary if you do not already have a user account. 40 | 3. Access the `Apps` tab from the side menu 41 | 4. Click on the `Deploy` button on the upper right corner 42 | 5. Fill in the user details of the account you are using (see step 2). 43 | 6. Browse or drap and drop the `app.zip` created by the build script. 44 | 7. Hit `Upload` 45 | 8. The app should show up on the app dashboard momentarily. Click on the `Launch` button to use the lottery demo app. 46 | -------------------------------------------------------------------------------- /src/components/Lottery/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { withRouter } from 'react-router-dom'; 5 | import { Card, CardTitle, CardText, Button } from 'react-md'; 6 | import LotteryDetails from '../LotteryDetails'; 7 | import Participate from '../Participate'; 8 | import { participateOpenModal } from '../Participate/participate.actions'; 9 | import './lottery.css'; 10 | 11 | class Lottery extends Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | isOpen: false 17 | } 18 | } 19 | 20 | handleModal(isOpen) { 21 | this.setState({ isOpen }); 22 | } 23 | 24 | winner(message, address) { 25 | return ( 26 | 33 | ) 34 | } 35 | 36 | render() { 37 | const isDisabled = (this.props.lotteryData.ticketCount - this.props.lotteryData.entries.length) <= 0; 38 | const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; 39 | const winner = this.props.lotteryData.winnerAddress !== '0000000000000000000000000000000000000000'; 40 | 41 | return ( 42 |
43 | 44 | 45 | 46 |

Jackpot: {this.props.lotteryData.ticketPrice * this.props.lotteryData.ticketCount} coins

47 |

Remaining Tickets: {this.props.lotteryData.ticketCount - this.props.lotteryData.entries.length}

48 |

Charity: {this.props.lotteryData.charityPercentage}%

49 |
50 | { winner ? (this.props.lotteryData.winnerAddress !== (user && user.address) ? this.winner('Completed', this.props.lotteryData.winnerAddress) : this.winner('You Won', this.props.lotteryData.winnerAddress) ) : 51 | } 52 |
53 |
54 |
55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | function mapStateToProps(state) { 62 | return {}; 63 | } 64 | 65 | const connected = connect( 66 | mapStateToProps, 67 | { 68 | participateOpenModal 69 | } 70 | )(Lottery); 71 | 72 | export default withRouter(connected); 73 | -------------------------------------------------------------------------------- /lib/lottery/raffle.js: -------------------------------------------------------------------------------- 1 | const ba = require('blockapps-rest'); 2 | const rest = ba.rest; 3 | const util = ba.common.util; 4 | const config = ba.common.config; 5 | const BigNumber = ba.common.BigNumber; 6 | const constants = ba.common.constants 7 | 8 | const contractName = 'Raffle'; 9 | const contractFilename = `./lib/lottery/contracts/Raffle.sol`; 10 | 11 | function* uploadContract(admin, args) { 12 | const contract = yield rest.uploadContract(admin, contractName, contractFilename, args); 13 | yield compileSearch(contract); 14 | contract.src = 'removed'; 15 | return setContract(admin, contract); 16 | } 17 | 18 | function setContract(admin, contract) { 19 | contract.getState = function* () { 20 | return yield rest.getState(contract); 21 | } 22 | contract.enter = function* (user) { 23 | return yield enter(admin, contract, user); 24 | } 25 | contract.testRand = function* (seed) { 26 | return yield testRand(admin, contract, seed); 27 | } 28 | return contract; 29 | } 30 | 31 | function* compileSearch(contract) { 32 | rest.verbose('compileSearch', contractName); 33 | if (yield rest.isCompiled(contract.codeHash)) { 34 | return; 35 | } 36 | const searchable = [contractName]; 37 | yield rest.compileSearch(searchable, contractName, contractFilename); 38 | } 39 | 40 | // ================== contract methods ==================== 41 | function* enter(admin, contract, user) { 42 | rest.verbose('enter', user); 43 | const state = yield rest.getState(contract); 44 | const ticketPrice = new BigNumber(state.ticketPrice); 45 | 46 | // function enter() payable return (bool) { 47 | const method = 'enter'; 48 | const args = {}; 49 | const value = ticketPrice.toFixed(); 50 | const result = yield rest.callMethod(user, contract, method, args, value); 51 | const success = (result[0] === true); 52 | return success; 53 | } 54 | 55 | function* testRand(admin, contract, seed) { 56 | rest.verbose('testRand', seed); 57 | // function rand(uint seed) internal returns (uint) 58 | // function testRand(uint seed) returns (uint) 59 | const method = 'testRand'; 60 | const args = { 61 | seed: seed, 62 | }; 63 | const result = yield rest.callMethod(admin, contract, method, args); 64 | const rand = parseInt(result); 65 | return rand; 66 | } 67 | 68 | // ================== wrapper methods ==================== 69 | function* getRaffle(address) { 70 | const results = (yield rest.waitQuery(`${contractName}?address=eq.${address}`, 1, 3*60*1000))[0]; 71 | return results; 72 | } 73 | 74 | function* getOpen() { 75 | const addressZero = '0000000000000000000000000000000000000000'; 76 | const results = yield rest.query(`${contractName}?winnerAddress=eq.${addressZero}`); 77 | return results; 78 | } 79 | 80 | // get all lotteries 81 | // get all open lotteries 82 | // upload (string) 83 | 84 | // function* getUsers(addresses) { 85 | // const csv = util.toCsv(addresses); // generate csv string 86 | // const results = yield rest.query(`${contractName}?address=in.${csv}`); 87 | // return results; 88 | // } 89 | 90 | 91 | module.exports = { 92 | uploadContract: uploadContract, 93 | compileSearch: compileSearch, 94 | // 95 | getRaffle: getRaffle, 96 | getOpen: getOpen, 97 | }; 98 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Participate/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { DialogContainer, Button } from 'react-md'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter } from 'react-router-dom'; 5 | import { Field, reduxForm, formValueSelector } from 'redux-form'; 6 | import { 7 | participateRequest, 8 | participateOpenModal, 9 | participateCloseModal 10 | } from './participate.actions'; 11 | import './participate.css' 12 | import ReduxedTextField from '../../components/ReduxedTextField'; 13 | import { buildContractName } from '../../raffle/raffle.js'; 14 | 15 | class Participate extends Component { 16 | submitting = false; 17 | handleOpenModal = (e) => { 18 | this.submitting = false; 19 | this.props.reset(); 20 | this.props.participateOpenModal(this.props.lookup); 21 | } 22 | 23 | handleCloseModal = (e) => { 24 | this.props.reset(); 25 | this.props.participateCloseModal(this.props.lookup); 26 | } 27 | 28 | submit = (values) => { 29 | const payload = { 30 | username: values.modalUsername, 31 | userAddress: values.modalAddress, 32 | password: values.modalPassword, 33 | contractName: buildContractName("Raffle"), 34 | contractAddress: this.props.lotteryData.address, 35 | methodName: "enter", 36 | value: values.modalValue * this.props.lotteryData.ticketPrice * 1000000000000000000, //convert to ether 37 | args: { 38 | _numTickets: values.modalValue, 39 | }, 40 | } 41 | this.submitting = this.props.participateRequest(this.props.lookup, payload).submitting; 42 | } 43 | 44 | render() { 45 | const handleSubmit = this.props.handleSubmit; 46 | const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; 47 | 48 | const error = this.props.failure && 49 | (
50 |
51 | 54 |
55 |
) 56 | 57 | const actions = [{ 58 | onClick: this.handleCloseModal, 59 | primary: true, 60 | children: 'Close', 61 | }, 62 | { 63 | onClick: handleSubmit(this.submit), 64 | disabled: this.props.pristine || this.props.submitting || this.submitting, 65 | primary: true, 66 | children: 'Play', 67 | className: this.submitting? 'disabled' : 'enabled', 68 | }]; 69 | return ( 70 |
71 | 77 |
78 | 88 |
89 |
90 |
91 | 94 |
95 | 105 |
106 |
107 |
108 | 111 |
112 | 122 |
123 |
124 |
125 | 128 |
129 | 138 |
139 |
140 |
141 | 144 |
145 | 154 |
155 |
156 |
157 | 160 |
161 |
162 | {error} 163 |
164 |
165 |
166 |
167 | ); 168 | } 169 | } 170 | 171 | function validate(values) { 172 | const errors = {}; 173 | 174 | if (!values.modalUsername) { 175 | errors.modalUsername = "Username Required"; 176 | } 177 | if (!values.modalAddress) { 178 | errors.modalAddress = "Address Required"; 179 | } 180 | if (!values.modalPassword) { 181 | errors.modalPassword = "Password Required"; 182 | } 183 | if (!values.modalValue) { 184 | errors.modalValue = "Tickets Required"; 185 | } 186 | if (values.modalValue < 1) { 187 | errors.modalValue = "Must have at least 1 ticket in lottery"; 188 | } 189 | if (!values.modalTotal) { 190 | errors.modalTotal = "Total Required"; 191 | } 192 | 193 | return errors; 194 | } 195 | 196 | const selector = formValueSelector('participate'); 197 | 198 | function mapStateToProps(state, ownProps) { 199 | const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; 200 | return { 201 | initialValues: { modalUsername: user && user.username, modalAddress: user && user.address }, 202 | modal: state.participate.modals 203 | && state.participate.modals[ownProps.lookup] ? 204 | state.participate.modals[ownProps.lookup] : {}, 205 | modalUsername: selector(state, 'modalUsername'), 206 | modalValue: selector(state, 'modalValue'), 207 | failure: state.participate.failure, 208 | }; 209 | } 210 | 211 | 212 | const formed = reduxForm({ form: 'participate', validate })(Participate); 213 | const connected = connect( 214 | mapStateToProps, 215 | { 216 | participateOpenModal, 217 | participateCloseModal, 218 | participateRequest, 219 | } 220 | )(formed); 221 | const routed = withRouter(connected); 222 | 223 | export default routed; 224 | -------------------------------------------------------------------------------- /src/raffle/raffle.js: -------------------------------------------------------------------------------- 1 | const contractName = buildContractName('Raffle') 2 | 3 | const uploadUrl = `http://${window.location.hostname}/bloc/v2.2/users/:user/:address/contract?resolve` 4 | const enterUrl = `http://${window.location.hostname}/bloc/v2.2/users/:username/:userAddress/contract/:contractName/:contractAddress/call?resolve`; 5 | // const raffleListUrl = `http://${window.location.hostname}/cirrus/search/Raffle?winnerAddress=eq.0000000000000000000000000000000000000000`; 6 | // const raffleListUrlNotEqual = `http://${window.location.hostname}/cirrus/search/Raffle?winnerAddress=not.eq.0000000000000000000000000000000000000000`; 7 | const raffleListUrlAll = `http://${window.location.hostname}/cirrus/search/${contractName}`; 8 | const cirrusUrl = `http://${window.location.hostname}/cirrus/search`; 9 | const compileUrl = `http://${window.location.hostname}/bloc/v2.2/contracts/compile`; 10 | const codeHash = 'a44d5968d33c8d99ef36ea6980a4151fd1fd45379a85425d55a71ccfb1860e57'; 11 | 12 | const contractSrc = `contract ${contractName} { 13 | address[] public entries; 14 | uint public ticketCount; 15 | uint public ticketPrice; 16 | string public name; 17 | 18 | uint public winner; 19 | address public winnerAddress; 20 | 21 | uint public charityPercentage; 22 | address public initiator; 23 | 24 | function ${contractName}(string _name, uint _ticketCount, uint _ticketPrice, uint _charityPercentage) { 25 | // if ticket count < 2 - whats the point 26 | if (_ticketCount < 2) { 27 | throw; 28 | } 29 | // all good 30 | name = _name; 31 | ticketCount = _ticketCount; 32 | ticketPrice = _ticketPrice; 33 | charityPercentage = _charityPercentage; 34 | winnerAddress = 0; 35 | initiator = msg.sender; 36 | } 37 | 38 | function enter(uint _numTickets) payable returns (bool) { 39 | // check if ticket price satisfied 40 | if (msg.value < ticketPrice * _numTickets) { 41 | return false; 42 | } 43 | // check capacity 44 | if (entries.length > ticketCount - _numTickets) { 45 | return false; 46 | } 47 | // enter the lottery 48 | for(uint i=0; i<_numTickets; i++) { 49 | entries.push(msg.sender); 50 | } 51 | // payout 52 | if (entries.length >= ticketCount) { 53 | return payout(); 54 | } 55 | return true; 56 | } 57 | 58 | /* return a random index into entries */ 59 | function rand(uint seed) internal returns (uint) { 60 | return uint(keccak256(seed)) % entries.length; 61 | } 62 | 63 | function testRand(uint seed) returns (uint) { 64 | if (entries.length < 2) { 65 | return 99999999; 66 | } 67 | return rand(seed); 68 | } 69 | 70 | function payout() internal returns (bool){ 71 | winner = rand(block.number); 72 | winnerAddress = entries[winner]; 73 | uint charityAmount = this.balance * charityPercentage / 100; 74 | winnerAddress.send(this.balance-charityAmount); 75 | initiator.send(charityAmount); 76 | return true; 77 | } 78 | }`; 79 | 80 | export function uploadContract(payload) { 81 | const url = uploadUrl.replace(":user", payload.username).replace(":address", payload.address); 82 | const body = JSON.stringify({ 83 | contract: contractName, 84 | value: 0, 85 | password: payload.password, 86 | src: contractSrc, 87 | args: payload.args 88 | }); 89 | const options = { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | }, 94 | body: body 95 | }; 96 | return fetch(url, options) 97 | .then(function (response) { 98 | if (response.ok) { 99 | return response.json(); 100 | } 101 | return response.text() 102 | .then((msg) => { 103 | if (msg === "") { 104 | throw new Error(`Error ${response.status} from POST to ${response.url}`) 105 | } 106 | throw new Error(msg) 107 | }); 108 | }) 109 | .then((json) => { 110 | isCompiled() 111 | .then((compiled) => { 112 | if (compiled) { 113 | return; 114 | } 115 | 116 | return compileSearch(contractName, contractSrc); 117 | }) 118 | .then(() => { 119 | return json; 120 | }); 121 | }) 122 | .catch(function (error) { 123 | throw error; 124 | }); 125 | 126 | //const contract = yield rest.uploadContract(admin, contractName, contractFilename, args); 127 | //yield compileSearch(contract); 128 | //contract.src = 'removed'; 129 | //return setContract(admin, contract); 130 | // console.log('uploadContract'); 131 | 132 | // const contract = { 133 | // address: args.address, 134 | // name: args.name, 135 | // numTickets: args.numTickets, 136 | // ticketPrice: args.ticketPrice, 137 | // numSold: 0, 138 | // }; 139 | 140 | // allRaffleData.push(contract); 141 | 142 | // console.log('Upload Contract'); 143 | // console.log(allRaffleData); 144 | 145 | // return contract; 146 | } 147 | 148 | export function setContract(admin, contract) { 149 | //contract.getState = function* () { 150 | // return yield rest.getState(contract); 151 | //} 152 | //contract.enter = function* (user) { 153 | // return yield enter(admin, contract, user); 154 | //} 155 | //contract.testRand = function* (seed) { 156 | // return yield testRand(admin, contract, seed); 157 | //} 158 | //return contract; 159 | console.log('setContract'); 160 | } 161 | 162 | export function isCompiled() { 163 | return fetch( 164 | `${cirrusUrl}/contract?codeHash=eq.${codeHash}`, { 165 | method: 'GET' 166 | } 167 | ) 168 | .then((response) => { 169 | return response.json(); 170 | }) 171 | .then((json) => { 172 | return json.length > 0; 173 | }) 174 | .catch((err) => { 175 | return false; 176 | }) 177 | } 178 | 179 | export function compileSearch() { 180 | return fetch( 181 | compileUrl, { 182 | method: 'POST', 183 | headers: { 184 | "content-type": "application/json" 185 | }, 186 | body: JSON.stringify([{ 187 | "contractName": contractName, 188 | "source": contractSrc, 189 | "searchable": [contractName] 190 | }]) 191 | } 192 | ) 193 | .then((response) => { 194 | return response.json(); 195 | }) 196 | } 197 | 198 | // ================== contract methods ==================== 199 | export function enter(payload) { 200 | // console.log('##################################### enter: ', payload); 201 | const url = enterUrl.replace(':username', payload.username) 202 | .replace(':userAddress', payload.userAddress) 203 | .replace(":contractName", payload.contractName) 204 | .replace(":contractAddress", payload.contractAddress); 205 | const body = JSON.stringify({ 206 | password: payload.password, 207 | method: payload.methodName, 208 | value: payload.value && !isNaN(parseFloat(payload.value)) ? payload.value : 0, 209 | args: payload.args, 210 | }); 211 | const options = { 212 | method: 'POST', 213 | headers: { 214 | 'Content-Type': 'application/json', 215 | }, 216 | body: body 217 | }; 218 | return fetch(url, options) 219 | .then(function (response) { 220 | if (response.ok) { 221 | return response.json(); 222 | } 223 | return response.text() 224 | .then((msg) => { 225 | if (msg === "") { 226 | throw new Error(`Error ${response.status} from POST to ${response.url}`) 227 | } 228 | throw msg 229 | }); 230 | }) 231 | .catch(function (error) { 232 | throw error; 233 | }); 234 | } 235 | 236 | export function* testRand(admin, contract, seed) { 237 | //rest.verbose('testRand', seed); 238 | // function rand(uint seed) internal returns (uint) 239 | // function testRand(uint seed) returns (uint) 240 | //const method = 'testRand'; 241 | //const args = { 242 | // seed: seed, 243 | //}; 244 | //const result = yield rest.callMethod(admin, contract, method, args); 245 | //const rand = parseInt(result); 246 | //return rand; 247 | } 248 | 249 | // ================== wrapper methods ==================== 250 | export function getRaffle(address) { 251 | // const results = (yield rest.waitQuery(`${contractName}?address=eq.${address}`, 1, 3*60*1000)); 252 | // if(results.length === 0) { 253 | // throw new Error("Not found"); 254 | // } 255 | // return results[0]; 256 | } 257 | 258 | export function getOpen() { 259 | const URL = raffleListUrlAll; 260 | return fetch( 261 | URL, { 262 | method: 'GET' 263 | }) 264 | .then(function (response) { 265 | return response.json(); 266 | }) 267 | .catch(function (error) { 268 | throw error; 269 | }); 270 | //const addressZero = '0000000000000000000000000000000000000000'; 271 | //const results = yield rest.query(`${contractName}?winnerAddress=eq.${addressZero}`); 272 | //return results; 273 | // console.log('getOpen'); 274 | 275 | // return allRaffleData; 276 | } 277 | 278 | // get all lotteries 279 | // get all open lotteries 280 | // upload (string) 281 | 282 | // function* getUsers(addresses) { 283 | // const csv = util.toCsv(addresses); // generate csv string 284 | // const results = yield rest.query(`${contractName}?address=in.${csv}`); 285 | // return results; 286 | // } 287 | 288 | export function buildContractName(defaultVal) { 289 | if (window.location.pathname.includes('app')) { 290 | const appHash = window.location.pathname.match(/apps\/(.*?)\//)[1] 291 | return `${defaultVal}_${appHash}` 292 | } 293 | return defaultVal 294 | } 295 | -------------------------------------------------------------------------------- /lib/lottery/test/lottery.test.js: -------------------------------------------------------------------------------- 1 | require('co-mocha'); 2 | const ba = require('blockapps-rest'); 3 | const rest = ba.rest; 4 | const common = ba.common; 5 | const config = common.config; 6 | const util = common.util; 7 | const should = common.should; 8 | const assert = common.assert; 9 | const Promise = common.Promise; 10 | const BigNumber = ba.common.BigNumber; 11 | const constants = ba.common.constants 12 | 13 | const lotteryJs = require('../lottery'); 14 | //const ErrorCodes = rest.getEnums(`${config.libPath}/common/ErrorCodes.sol`).ErrorCodes; 15 | 16 | const adminName = util.uid('Admin'); 17 | const adminPassword = '1234'; 18 | 19 | describe('Lottery tests', function() { 20 | this.timeout(config.timeout); 21 | 22 | let admin; 23 | 24 | before(function* () { 25 | admin = yield rest.createUser(adminName, adminPassword); 26 | }); 27 | 28 | /* function Lottery(uint _totalValue) { */ 29 | 30 | it('Create Contract - constructor arguments', function* () { 31 | const ticketCount = 5; 32 | const ticketPrice = new BigNumber(1234567890).mul(constants.ETHER); 33 | 34 | const args = { 35 | _ticketCount: ticketCount, 36 | _ticketPrice: ticketPrice.toFixed(), 37 | }; 38 | 39 | const contract = yield lotteryJs.uploadContract(admin, args); 40 | // state 41 | { 42 | const lottery = yield contract.getState(); 43 | assert.equal(lottery.ticketCount, ticketCount, 'ticketCount'); 44 | assert.equal(lottery.ticketPrice, ticketPrice.toFixed(), 'ticketPrice'); 45 | assert.equal(lottery.winnerAddress, 0, 'winnerAddress'); 46 | } 47 | // query 48 | { 49 | const lottery = yield lotteryJs.getLottery(contract.address); 50 | assert.equal(lottery.ticketCount, ticketCount, 'ticketCount'); 51 | assert.equal(lottery.ticketPrice, ticketPrice.toFixed(), 'ticketPrice'); 52 | assert.equal(lottery.winnerAddress, 0, 'winnerAddress'); 53 | } 54 | }); 55 | 56 | //enter - send money - see that it went in 57 | it('enter - enter an open lottery', function* () { 58 | const ticketCount = 10; 59 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 60 | const contract = yield createContract(admin, ticketCount, ticketPrice); 61 | 62 | const username = util.uid('User'); 63 | const user = yield rest.createUser(username, adminPassword); 64 | 65 | const result = yield contract.enter(user); 66 | assert.isTrue(result, 'entered'); 67 | 68 | const state = yield contract.getState(); 69 | assert.equal(user.address, state.entries[0]); 70 | }); 71 | 72 | //reach cap - close loterry 73 | it('enter - enter an open lottery until capacity', function* () { 74 | const ticketCount = 3; 75 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 76 | const contract = yield createContract(admin, ticketCount, ticketPrice); 77 | 78 | // enter totalValue times 79 | for (let i = 0; i < ticketCount; i++) { 80 | const result = yield contract.enter(admin); 81 | assert.isTrue(result, 'entered ' + i); 82 | } 83 | // should reach capacity 84 | const result = yield contract.enter(admin); 85 | assert.isFalse(result, 'should reach capacity'); 86 | }); 87 | 88 | it('enter - transfer value', function* () { 89 | const ticketCount = 3; 90 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 91 | const contract = yield createContract(admin, ticketCount, ticketPrice); 92 | 93 | // check balances 94 | admin.startBalance = yield rest.getBalance(admin.address); 95 | contract.startBalance = yield rest.getBalance(contract.address); 96 | 97 | // enter 98 | const result = yield contract.enter(admin); 99 | assert.isTrue(result, 'entered'); 100 | 101 | // check balances 102 | admin.endBalance = yield rest.getBalance(admin.address); 103 | admin.delta = admin.endBalance.minus(admin.startBalance).mul(-1); 104 | admin.delta.should.be.bignumber.gt(ticketPrice); 105 | 106 | contract.endBalance = yield rest.getBalance(contract.address); 107 | contract.delta = contract.endBalance.minus(contract.startBalance); 108 | contract.delta.should.be.bignumber.equal(ticketPrice); 109 | }); 110 | 111 | it('enter - transfer value - INSUFFICIENT BALANCE', function* () { 112 | // check balances 113 | admin.startBalance = yield rest.getBalance(admin.address); 114 | 115 | const ticketCount = 3; 116 | const ticketPrice = admin.startBalance.plus(12340000000); 117 | const contract = yield createContract(admin, ticketCount, ticketPrice); 118 | 119 | // enter - INSUFFICIENT BALANCE 120 | let result; 121 | try { 122 | result = yield contract.enter(admin); 123 | } catch(error) { 124 | assert.isTrue(error.name.includes('HttpError')); 125 | assert.equal(error.status, '400'); 126 | } 127 | // if didnt throw - error 128 | assert.isUndefined(result, 'call should have thrown INSUFFICIENT BALANCE, and not return a result'); 129 | }); 130 | 131 | it('enter - transfer value - below ticket price', function* () { 132 | const ticketCount = 3; 133 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 134 | const contract = yield createContract(admin, ticketCount, ticketPrice); 135 | 136 | // function enter() return (bool) { 137 | const method = 'enter'; 138 | const args = {}; 139 | const value = ticketPrice.minus(66600000).toFixed(); 140 | const result = yield rest.callMethod(admin, contract, method, args, value); 141 | const success = (result[0] === true); 142 | assert.isFalse(success, 'should be rejected'); 143 | }); 144 | 145 | it('rand tester', function* () { 146 | const ticketCount = 3; 147 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 148 | const contract = yield createContract(admin, ticketCount, ticketPrice); 149 | const seed = 0; 150 | 151 | const noWay = yield contract.testRand(seed); 152 | assert.isAtLeast(noWay, 999999, 'premature call should abort'); 153 | 154 | for (let i = 0; i < ticketCount; i++) { 155 | const result = yield contract.enter(admin); 156 | assert.isTrue(result, 'entered ' + i); 157 | } 158 | 159 | const rand = yield contract.testRand(seed); 160 | assert.isAtLeast(rand, 0, 'above 0'); 161 | assert.isAtMost(rand, ticketCount, 'within range'); 162 | }); 163 | 164 | it.skip('rand tester (multiple sample)', function* () { 165 | const ticketCount = 10; 166 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 167 | const contract = yield createContract(admin, ticketCount, ticketPrice); 168 | 169 | for (let i = 0; i < ticketCount; i++) { 170 | const result = yield contract.enter(admin); 171 | assert.isTrue(result, 'entered ' + i); 172 | } 173 | 174 | const dist = Array.apply(null, { 175 | length: ticketCount 176 | }).map(function(item, index) { 177 | return 0; 178 | }); 179 | for (let i = 0; i < ticketCount * 10; i++) { 180 | const rand = yield contract.testRand(i); 181 | assert.isAtLeast(rand, 0, 'above 0'); 182 | assert.isAtMost(rand, ticketCount, 'within range'); 183 | dist[rand]++; 184 | } 185 | }); 186 | 187 | it('enter - for the win', function* () { 188 | const ticketCount = 10; 189 | const ticketPrice = new BigNumber(1).mul(constants.ETHER); 190 | const contract = yield createContract(admin, ticketCount, ticketPrice); 191 | const uid = util.uid(); 192 | 193 | const userNames = Array.apply(null, { 194 | length: ticketCount, 195 | }).map(function(item, index) { 196 | return 'User_' + uid + '_' + index; 197 | }); 198 | 199 | const users = []; 200 | for (let i = 0; i < userNames.length; i++) { 201 | // create user 202 | const user = yield rest.createUser(userNames[i], adminPassword); 203 | users.push(user); 204 | // record balance for winner check 205 | user.balanceStart = yield rest.getBalance(user.address); 206 | // enter 207 | const result = yield contract.enter(user); 208 | assert.isTrue(result, 'entered ' + user.username); 209 | } 210 | 211 | const prize = ticketPrice.mul(ticketCount); 212 | const estimatedFees = 100000; 213 | const estimatedPrize = prize.minus(estimatedFees); 214 | let winners = 0; 215 | for (let i = 0; i < users.length; i++) { 216 | const user = users[i]; 217 | user.balanceEnd = yield rest.getBalance(user.address); 218 | const delta = user.balanceEnd.minus(user.balanceStart); 219 | const expected = estimatedPrize.minus(ticketPrice); 220 | if (delta.gt(expected)) { // 221 | winners++; 222 | } 223 | } 224 | const state = yield contract.getState(); 225 | assert.equal(winners, 1, 'one and only one'); 226 | }); 227 | 228 | //reach cap - close loterry 229 | it('find all open lotteries', function* () { 230 | const count = 10; 231 | const contracts = []; 232 | 233 | for (let i = 0; i < count; i++) { 234 | const ticketCount = 10+i; 235 | const ticketPrice = new BigNumber(10+i).mul(constants.ETHER); 236 | const contract = yield createContract(admin, ticketCount, ticketPrice); 237 | contracts.push(contract); 238 | } 239 | 240 | // query 241 | const result = yield lotteryJs.getOpen(); 242 | const comparator = function(oA, oB) { return oA.address == oB.address; }; 243 | const notFound = util.filter.isContained(contracts, result, comparator, true); 244 | assert.equal(notFound.length, 0, JSON.stringify(notFound)); 245 | }); 246 | }); 247 | 248 | function* createContract(admin, ticketCount, ticketPrice) { 249 | const args = { 250 | _ticketCount: ticketCount, 251 | _ticketPrice: ticketPrice.toFixed(), 252 | }; 253 | const contract = yield lotteryJs.uploadContract(admin, args); 254 | const searchResult = yield lotteryJs.getLottery(contract.address); // blocks until found 255 | return contract; 256 | } 257 | -------------------------------------------------------------------------------- /src/components/NewLottery/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, DialogContainer } from 'react-md'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter } from 'react-router-dom'; 5 | import { Field, reduxForm } from 'redux-form'; 6 | import ReduxedTextField from '../../components/ReduxedTextField'; 7 | import { 8 | newLotteryCall, 9 | newLotteryOpenModal, 10 | newLotteryCloseModal 11 | } from './newlottery.actions'; 12 | import './newLottery.css'; 13 | 14 | class NewLottery extends Component { 15 | submitting = false; 16 | 17 | handleOpenModal = (e) => { 18 | this.submitting = false; 19 | this.props.reset(); 20 | this.props.newLotteryOpenModal(); 21 | } 22 | 23 | handleCloseModal = (e) => { 24 | this.props.reset(); 25 | this.props.newLotteryCloseModal(); 26 | } 27 | 28 | submit = (values) => { 29 | const payload = { 30 | admin: values.modalUsername, 31 | username: values.modalUsername, 32 | address: values.modalAddress, 33 | password: values.modalPassword, 34 | args: { 35 | _name: values.modalName, 36 | _description: values.modalRafalInfo, 37 | _ticketCount: values.modalValue, 38 | _ticketPrice: values.modalTicketPrice, 39 | _charityPercentage: values.modalCharity 40 | } 41 | } 42 | this.submitting = this.props.newLotteryCall(payload).submitting; 43 | } 44 | 45 | render() { 46 | const handleSubmit = this.props.handleSubmit; 47 | const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; 48 | if (this.props.failure && this.submitting) { 49 | this.submitting = false; 50 | } 51 | 52 | const actions = [{ 53 | onClick: this.handleCloseModal, 54 | primary: true, 55 | children: 'Close', 56 | }, 57 | { 58 | onClick: handleSubmit(this.submit), 59 | disabled: this.submitting, 60 | primary: true, 61 | children: 'Submit', 62 | className: this.submitting ? 'disabled' : 'enabled', 63 | }]; 64 | 65 | const error = this.props.failure && 66 | (
67 | 70 |
) 71 | 72 | return ( 73 |
74 | 77 |
78 | 89 |
90 |
91 |
92 | 95 |
96 | 106 |
107 |
108 |
109 | 112 |
113 | 123 |
124 |
125 |
126 | 129 |
130 | 139 |
140 |
141 |
142 | 145 |
146 | 155 |
156 |
157 |
158 | 161 |
162 | 171 |
172 |
173 |
174 | 177 |
178 | 186 |
187 |
188 |
189 | 192 |
193 | 202 |
203 |
204 |
205 | 208 |
209 | 218 |
219 | {error} 220 |
221 |
222 |
223 |
224 | ); 225 | } 226 | } 227 | 228 | 229 | function mapStateToProps(state) { 230 | const user = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null; 231 | 232 | return { 233 | initialValues: { modalUsername: user && user.username, modalAddress: user && user.address }, 234 | isOpen: state.newLottery.isOpen, 235 | failure: state.newLottery.failure, 236 | modalUsername: '', 237 | }; 238 | } 239 | 240 | function validate(values) { 241 | const errors = {}; 242 | 243 | if (!values.modalUsername) { 244 | errors.modalUsername = "Username Required"; 245 | } 246 | if (!values.modalAddress) { 247 | errors.modalAddress = "Address Required"; 248 | } 249 | if (!values.modalPassword) { 250 | errors.modalPassword = "Password Required"; 251 | } 252 | if (!values.modalName) { 253 | errors.modalName = "Name required"; 254 | } 255 | if (!/^.{10,59}$/.test(values.modalName)) { 256 | errors.modalName = " Raffle name must be at least 10 characters and less than 60 characters"; 257 | } 258 | if (!values.modalRafalInfo) { 259 | errors.modalRafalInfo = "Raffle info required"; 260 | } 261 | if (!/^.{10,59}$/.test(values.modalRafalInfo)) { 262 | errors.modalRafalInfo = " Raffle description must be at least 10 characters and less than 60 characters"; 263 | } 264 | if (!values.modalTicketPrice) { 265 | errors.modalTicketPrice = "Ticket price required"; 266 | } 267 | if (!/^.{1,10}$/.test(values.modalTicketPrice)) { 268 | errors.modalTicketPrice = "Ticket price must be at least 1 characters and less than 10 characters"; 269 | } 270 | if (!values.modalCharity) { 271 | errors.modalCharity = "Charity Required"; 272 | } 273 | if (!(/^.{1,10}$/.test(values.modalCharity))) { 274 | errors.modalCharity = "Less than 10 characters"; 275 | } 276 | if (!values.modalValue) { 277 | errors.modalValue = "Tickets Required"; 278 | } 279 | if (values.modalValue < 2) { 280 | errors.modalValue = "Must have more than 1 ticket in lottery"; 281 | } 282 | 283 | return errors; 284 | } 285 | 286 | const formed = reduxForm({ form: 'newLottery', validate })(NewLottery); 287 | const connected = connect( 288 | mapStateToProps, 289 | { 290 | newLotteryOpenModal, 291 | newLotteryCloseModal, 292 | newLotteryCall, 293 | } 294 | )(formed); 295 | const routed = withRouter(connected); 296 | 297 | export default routed; 298 | --------------------------------------------------------------------------------