├── tsconfig.prod.json ├── images.d.ts ├── src ├── index.css ├── store │ ├── actions.ts │ ├── web3client.ts │ └── index.ts ├── App.test.tsx ├── App.tsx ├── index.tsx ├── App.css ├── abi │ └── MyCurrency.js ├── logo.svg ├── containers │ └── MyCurrency.tsx └── registerServiceWorker.ts ├── public ├── favicon.ico ├── manifest.json └── index.html ├── tsconfig.test.json ├── tslint.json ├── README.md ├── .gitignore ├── smartcontracts ├── simplesm.sol └── smscript.js ├── tsconfig.json └── package.json /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json" 3 | } -------------------------------------------------------------------------------- /images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/smart-contract-demo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { action } from 'typesafe-actions'; 2 | import { SET_ACCOUNT_INFO } from '.'; 3 | 4 | export function setAccountInfo(account: string, token: number) { 5 | return action(SET_ACCOUNT_INFO, { 6 | account, 7 | token 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": [ 5 | "config/**/*.js", 6 | "node_modules/**/*.ts", 7 | "coverage/lcov-report/*.js" 8 | ] 9 | }, 10 | "rules": { 11 | "no-console": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is an example to create smart contract and user interface using. 2 | * NodeJs/Typescript 3 | * React/Redux 4 | * antd UI library 5 | * Solidity Smart Contract 6 | * ganache-cli (for local blockchain) 7 | * web3 8 | 9 | See tutorial here https://medium.com/@pongsatt/how-to-develop-user-interface-for-smart-contract-using-react-1108314ebeeb -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | ganachedb/ 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Row } from 'antd'; 2 | import * as React from 'react'; 3 | import './App.css'; 4 | import MyCurrency from './containers/MyCurrency'; 5 | 6 | class App extends React.Component { 7 | public render() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | import { Provider } from 'react-redux'; 8 | import store from './store'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') as HTMLElement 15 | ); 16 | registerServiceWorker(); 17 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .App { 4 | text-align: center; 5 | } 6 | 7 | .App-logo { 8 | animation: App-logo-spin infinite 20s linear; 9 | height: 80px; 10 | } 11 | 12 | .App-header { 13 | background-color: #222; 14 | height: 150px; 15 | padding: 20px; 16 | color: white; 17 | } 18 | 19 | .App-title { 20 | font-size: 1.5em; 21 | } 22 | 23 | .App-intro { 24 | font-size: large; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { transform: rotate(0deg); } 29 | to { transform: rotate(360deg); } 30 | } 31 | -------------------------------------------------------------------------------- /smartcontracts/simplesm.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.0; 2 | contract MyCurrency { 3 | 4 | string public name; 5 | mapping(address => uint) public balanceOf; 6 | 7 | constructor(string _name, uint _initialAmount) public { 8 | name = _name; 9 | balanceOf[msg.sender] = _initialAmount; 10 | } 11 | 12 | function transfer(uint _amount, address _to) public { 13 | require (balanceOf[msg.sender] >= _amount, "You don't have enough amount."); 14 | 15 | balanceOf[_to] += _amount; 16 | balanceOf[msg.sender] -= _amount; 17 | } 18 | } -------------------------------------------------------------------------------- /src/abi/MyCurrency.js: -------------------------------------------------------------------------------- 1 | export default [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_amount","type":"uint256"},{"name":"_to","type":"address"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_name","type":"string"},{"name":"_initialAmount","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "build/dist", 5 | "module": "esnext", 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "rootDir": "src", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "build", 24 | "scripts", 25 | "acceptance-tests", 26 | "webpack", 27 | "jest", 28 | "src/setupTests.ts", 29 | "smartcontracts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/store/web3client.ts: -------------------------------------------------------------------------------- 1 | const web3 = (window as any).web3; 2 | 3 | // replace this with address from step above 4 | const contractAddr = '0x09233d0f7c706D7F9B0Cba18687fB16c49EcD65d'; 5 | 6 | import MyCurrency from "../abi/MyCurrency"; 7 | const tokenContract: any = getContract(MyCurrency, contractAddr); 8 | 9 | export function getContract(abi: object, address: string) { 10 | return web3.eth.contract(abi).at(address); 11 | } 12 | 13 | export function getAccount(): string { 14 | return web3.eth.defaultAccount; 15 | } 16 | 17 | export async function getToken(address?: string): Promise { 18 | address = address || getAccount(); 19 | const result = await promisify((f) => tokenContract.balanceOf(address, f)); 20 | return result.toNumber(); 21 | } 22 | 23 | export function promisify(fn: (cb: any) => any): Promise { 24 | return new Promise((resolve, reject) => { 25 | fn((err: any, result: any) => { 26 | if (err) { 27 | return reject(err); 28 | } 29 | 30 | resolve(result); 31 | }); 32 | }); 33 | } 34 | 35 | export async function transferToken(amount: number, to: string): Promise { 36 | await promisify((f) => tokenContract.transfer(amount, to, f)); 37 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3prj", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^3.8.2", 7 | "react": "^16.4.2", 8 | "react-dom": "^16.4.2", 9 | "react-redux": "^5.0.7", 10 | "react-scripts-ts": "2.17.0", 11 | "redux": "^4.0.0", 12 | "typesafe-actions": "^2.0.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts-ts start", 16 | "build": "react-scripts-ts build", 17 | "test": "react-scripts-ts test --env=jsdom", 18 | "eject": "react-scripts-ts eject", 19 | "ethserve": "mkdir -p ganachedb && ganache-cli --db ganachedb --account=\"0xbfe038810d51b8a788e5f63d2a0822d1f8ef712a7acbbf9877013efb3c5f5514,100000000000000000000\" --account=\"0xb07210851b10f884f5b4e57a8e76e717cc5cd9b496801cb93dac380747239b0f,100000000000000000000\" --account=\"0x46c855cb4a30c5630b5884fde176fe6d4b2c26f12c8607452c72ca31c537120e,100000000000000000000\"", 20 | "deploysm": "node ./smartcontracts/smscript.js" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^23.3.1", 24 | "@types/node": "^10.7.1", 25 | "@types/react": "^16.4.11", 26 | "@types/react-dom": "^16.0.7", 27 | "@types/react-redux": "^6.0.6", 28 | "@types/web3": "^1.0.3", 29 | "ganache-cli": "^6.1.8", 30 | "solc": "^0.4.24", 31 | "typescript": "^3.0.1", 32 | "web3": "^1.0.0-beta.35" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /smartcontracts/smscript.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const solc = require('solc'); 3 | const content = fs.readFileSync('./smartcontracts/simplesm.sol'); 4 | const input = {'simplesm.sol': content.toString()}; 5 | const output = solc.compile({sources: input}, 1); 6 | 7 | // use output from previous step 8 | // Connect to local server 9 | const Web3 = require('Web3'); 10 | const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); 11 | 12 | // Get contract interface 13 | const contractKey = 'simplesm.sol:MyCurrency'; 14 | const bytecode = output.contracts[contractKey].bytecode; 15 | const data = '0x' + bytecode; 16 | const abiJson = output.contracts[contractKey].interface; 17 | const abi = JSON.parse(abiJson); 18 | // Deploy 19 | const Contract = new web3.eth.Contract(abi); 20 | const tokenName = 'MyToken'; 21 | const initialTokens = 100; 22 | Contract.deploy({data, arguments: [tokenName, initialTokens]}) 23 | .send({ 24 | from: '0x954c8e10ed7eab4e467085f876b9d4534a488737', 25 | gas: 4700000 26 | }) 27 | .on('error', function (error) { console.log('error: ', error) }) 28 | .then(function (newContract) { 29 | const address = newContract.options.address; 30 | console.log('address:', address) 31 | }); 32 | 33 | const abidir = 'src/abi/'; 34 | 35 | if (!fs.existsSync(abidir)){ 36 | fs.mkdirSync(abidir); 37 | } 38 | fs.writeFileSync(abidir + 'MyCurrency.js', 'export default ' + abiJson); -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, createStore, Dispatch } from 'redux'; 2 | import { ActionType } from 'typesafe-actions'; 3 | import * as actions from './actions'; 4 | import * as web3client from './web3client'; 5 | 6 | export const SET_ACCOUNT_INFO = 'SET_ACCOUNT_INFO'; 7 | 8 | export type ActionTypes = ActionType; 9 | 10 | export interface IMyCurrency { 11 | account: string 12 | token: number 13 | } 14 | 15 | export interface IRootState { 16 | mycurrency: IMyCurrency 17 | } 18 | 19 | export function sharesReducer(state: IMyCurrency = {account: '', token: 0}, action: ActionTypes): IMyCurrency { 20 | 21 | switch (action.type) { 22 | case SET_ACCOUNT_INFO: 23 | return { ...state, ...action.payload }; 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export async function loadAccountInfo(dispatch: Dispatch) { 30 | const account = web3client.getAccount(); 31 | const token = await web3client.getToken(account); 32 | dispatch(actions.setAccountInfo(account, token)); 33 | } 34 | 35 | export async function transferToken(dispatch: Dispatch, to: string, amount: number) { 36 | await web3client.transferToken(amount, to); 37 | 38 | await loadAccountInfo(dispatch); 39 | } 40 | 41 | const store = createStore( 42 | combineReducers({ 43 | mycurrency: sharesReducer 44 | })); 45 | 46 | export default store; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/containers/MyCurrency.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Icon, Input } from 'antd'; 2 | import { FormComponentProps } from 'antd/lib/form'; 3 | import * as React from 'react'; 4 | import { connect } from 'react-redux'; 5 | import { Dispatch } from 'redux'; 6 | import { IRootState, loadAccountInfo, transferToken } from '../store'; 7 | 8 | const FormItem = Form.Item; 9 | 10 | class MyCurrency extends React.Component { 11 | public handleSubmit = (e: React.FormEvent) => { 12 | e.preventDefault(); 13 | this.props.form.validateFields((err, values) => { 14 | if (!err) { 15 | const {to, amount} = values; 16 | this.props.transfer(to, amount); 17 | } 18 | }); 19 | } 20 | 21 | public componentDidMount() { 22 | this.props.loadAccount(); 23 | } 24 | 25 | public render() { 26 | const { account, token } = this.props; 27 | const { getFieldDecorator } = this.props.form; 28 | return ( 29 |
30 | 31 | {getFieldDecorator('account', { 32 | initialValue: account 33 | })( 34 | } placeholder="Account" disabled={true} /> 35 | )} 36 | 37 | 38 | {getFieldDecorator('token', { 39 | initialValue: token 40 | })( 41 | } disabled={true} /> 42 | )} 43 | 44 | 45 | {getFieldDecorator('to', { 46 | initialValue: '0xb07210851b10f884f5b4e57a8e76e717cc5cd9b496801cb93dac380747239b0f' 47 | })( 48 | } placeholder="To account"/> 49 | )} 50 | 51 | 52 | {getFieldDecorator('amount', { 53 | initialValue: 1 54 | })( 55 | } placeholder="Number of token" /> 56 | )} 57 | 58 | 59 | 62 | 63 |
64 | ); 65 | } 66 | } 67 | 68 | type ReduxProps = ReturnType & ReturnType; 69 | 70 | export function mapStateToProps({ mycurrency }: IRootState) { 71 | return {...mycurrency}; 72 | } 73 | 74 | export function mapDispatchToProps(dispatch: Dispatch) { 75 | return { 76 | loadAccount: () => loadAccountInfo(dispatch), 77 | transfer: (to: string, amount: number) => transferToken(dispatch, to, amount) 78 | }; 79 | } 80 | 81 | export default connect(mapStateToProps, mapDispatchToProps)(Form.create()(MyCurrency)); -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log('New content is available; please refresh.'); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log('Content is cached for offline use.'); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type')!.indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | 'No internet connection found. App is running in offline mode.' 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ('serviceWorker' in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | --------------------------------------------------------------------------------