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