diff --git a/app/js/actions/bucket.ts b/app/js/actions/bucket.ts
new file mode 100644
index 0000000..0e1e3a5
--- /dev/null
+++ b/app/js/actions/bucket.ts
@@ -0,0 +1,159 @@
+import { RootState } from '../reducers';
+import GiftBucket from '../../../embarkArtifacts/contracts/GiftBucket';
+import IERC20Detailed from '../../../embarkArtifacts/contracts/IERC20Detailed';
+import { config } from "../config";
+import { Contract } from 'web3-eth-contract';
+import { Dispatch } from 'redux';
+
+export const ERROR_GIFT_NOT_FOUND = "ERROR_GIFT_NOT_FOUND";
+export interface ErrGiftNotFound {
+ type: typeof ERROR_GIFT_NOT_FOUND
+}
+
+export const ERROR_LOADING_GIFT = "ERROR_LOADING_GIFT";
+export interface ErrLoadingGift {
+ type: typeof ERROR_LOADING_GIFT
+ message: string
+}
+
+export type BucketError =
+ ErrGiftNotFound;
+
+const errGiftNotFound = () => ({
+ type: ERROR_GIFT_NOT_FOUND,
+});
+
+const errLoadingGift = (message: string) => ({
+ type: ERROR_LOADING_GIFT,
+ message,
+});
+
+export const BUCKET_GIFT_LOADING = "BUCKET_GIFT_LOADING";
+export interface BucketGiftLoadingAction {
+ type: typeof BUCKET_GIFT_LOADING
+ address: string
+}
+
+export const BUCKET_GIFT_LOADING_ERROR = "BUCKET_GIFT_LOADING_ERROR";
+export interface BucketGiftLoadingErrorAction {
+ type: typeof BUCKET_GIFT_LOADING_ERROR
+ error: BucketError
+}
+
+export const BUCKET_GIFT_LOADED = "BUCKET_GIFT_LOADED";
+export interface BucketGiftLoadedAction {
+ type: typeof BUCKET_GIFT_LOADED
+ recipient: string
+ amount: string
+ codeHash: string
+}
+
+export const BUCKET_GIFT_NOT_FOUND = "BUCKET_GIFT_NOT_FOUND";
+export interface BucketGiftNotFoundAction {
+ type: typeof BUCKET_GIFT_NOT_FOUND
+ error: BucketError
+}
+
+export const BUCKET_TOKEN_LOADING = "BUCKET_TOKEN_LOADING";
+export interface BucketTokenLoadingAction {
+ type: typeof BUCKET_TOKEN_LOADING
+ address: string
+}
+
+export const BUCKET_TOKEN_LOADED = "BUCKET_TOKEN_LOADED";
+export interface BucketTokenLoadedAction {
+ type: typeof BUCKET_TOKEN_LOADED
+ symbol: string
+ decimal: number
+}
+
+export type BucketActions =
+ BucketGiftLoadingAction |
+ BucketGiftLoadingErrorAction |
+ BucketGiftLoadedAction |
+ BucketGiftNotFoundAction |
+ BucketTokenLoadingAction |
+ BucketTokenLoadedAction;
+
+export const loadingGift = (address: string): BucketLoadingAction => ({
+ type: BUCKET_GIFT_LOADING,
+ address,
+});
+
+export const giftLoaded = (recipient: string, amount: string, codeHash: string): BucketGiftLoadedAction => ({
+ type: BUCKET_GIFT_LOADED,
+ recipient,
+ amount,
+ codeHash,
+});
+
+export const giftNotFound = (recipient: string, amount: string, codeHash: string): BucketGiftNotFoundAction => ({
+ type: BUCKET_GIFT_NOT_FOUND,
+ error: errGiftNotFound(),
+});
+
+export const errorLoadingGift = (errorMessage: string): BucketGiftNotFoundAction => ({
+ type: BUCKET_GIFT_NOT_FOUND,
+ error: errLoadingGift(errorMessage),
+});
+
+export const loadingToken = (address: string): BucketTokenLoadingAction => ({
+ type: BUCKET_TOKEN_LOADING,
+ address,
+});
+
+export const tokenLoaded = (symbol: string, decimals: number): BucketTokenLoadedAction => ({
+ type: BUCKET_TOKEN_LOADED,
+ symbol,
+ decimals,
+});
+
+const newBucketContract = (address: string) => {
+ const bucketAbi = GiftBucket.options.jsonInterface;
+ const bucket = new config.web3!.eth.Contract(bucketAbi, address);
+ return bucket;
+}
+
+const newERC20Contract = (address: string) => {
+ const erc20Abi = IERC20Detailed.options.jsonInterface;
+ const erc20 = new config.web3!.eth.Contract(erc20Abi, address);
+ return erc20;
+}
+
+export const loadGift = (bucketAddress: string, recipientAddress: string) => {
+ return async (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch(loadingGift(bucketAddress, recipientAddress));
+ const bucket = newBucketContract(bucketAddress);
+
+ bucket.methods.gifts(recipientAddress).call().then((result: Any) => {
+ const { recipient, amount, code } = result;
+ if (amount === "0") {
+ dispatch(giftNotFound())
+ return;
+ }
+
+ dispatch(giftLoaded(recipient, amount, code));
+ dispatch(loadToken(bucket))
+ }).catch(err => {
+ dispatch(errorLoadingGift(err))
+ console.error("err: ", err)
+ })
+ };
+};
+
+export const loadToken = (bucket: Contract) => {
+ return (dispatch: Dispatch, getState: () => RootState) => {
+ bucket.methods.tokenContract().call().then(async (address: string) => {
+ const erc20Abi = IERC20Detailed.options.jsonInterface;
+ const erc20 = new config.web3!.eth.Contract(erc20Abi, address);
+ dispatch(loadingToken(address));
+
+ const symbol = await erc20.methods.symbol().call();
+ const decimals = await erc20.methods.decimals().call();
+ dispatch(tokenLoaded(symbol, decimals));
+ }).catch((err: string) => {
+ //FIXME: manage error
+ console.error("ERROR: ", err);
+ })
+ }
+}
diff --git a/app/js/actions/web3.js b/app/js/actions/web3.js
deleted file mode 100644
index 9b0c915..0000000
--- a/app/js/actions/web3.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import Web3 from 'web3';
-import { Dispatch } from 'redux';
-import { config } from '../config';
-
-export const VALID_NETWORK_NAME = "Ropsten";
-export const VALID_NETWORK_ID = 3;
-// export const VALID_NETWORK_NAME = "Goerli";
-// export const VALID_NETWORK_ID = 5;
-export const LOCAL_NETWORK_ID = 1337;
-
-export const WEB3_INITIALIZED = "WEB3_INITIALIZED";
-export const WEB3_ERROR = "WEB3_ERROR";
-export const WEB3_NETWORK_ID_LOADED = "WEB3_NETWORK_ID_LOADED";
-export const WEB3_ACCOUNT_LOADED = "WEB3_ACCOUNT_LOADED";
-
-export const web3Initialized = () => ({
- type: WEB3_INITIALIZED,
-})
-
-export const web3NetworkIDLoaded = networkID => ({
- type: WEB3_NETWORK_ID_LOADED,
- networkID,
-});
-
-export const web3Error = error => ({
- type: WEB3_ERROR,
- error,
-});
-
-export const web3AccoutLoaded = account => ({
- type: WEB3_ACCOUNT_LOADED,
- account,
-});
-
-export const initWeb3 = () => {
- if (window.ethereum) {
- config.web3 = new Web3(window.ethereum);
- return (dispatch, getState) => {
- window.ethereum.enable()
- .then(() => {
- dispatch(web3Initialized());
- dispatch(loadNetwordId());
- })
- .catch((err) => {
- dispatch(web3Error(err));
- });
- }
- } else if (window.web3) {
- config.web3 = window.web3;
- return (dispatch, getState) => {
- dispatch(web3Initialized());
- dispatch(loadNetwordId());
- }
- } else {
- //FIXME: move to config
- // const web3 = new Web3('https://ropsten.infura.io/v3/f315575765b14720b32382a61a89341a');
- // const web3 = new Web3(new Web3.providers.HttpProvider('https://ropsten.infura.io/v3/f315575765b14720b32382a61a89341a'));
- config.web3 = new Web3(new Web3.providers.WebsocketProvider('wss://ropsten.infura.io/ws/v3/f315575765b14720b32382a61a89341a'));
- return (dispatch, getState) => {
- dispatch(web3Initialized());
- dispatch(loadNetwordId());
- }
- }
-}
-
-const loadNetwordId = () => {
- return (dispatch, getState) => {
- config.web3.eth.net.getId().then((id) => {
- dispatch(web3NetworkIDLoaded(id))
- if (id !== VALID_NETWORK_ID && id !== LOCAL_NETWORK_ID) {
- dispatch(web3Error(`wrong network, please connect to ${VALID_NETWORK_NAME}`));
- return;
- }
-
- dispatch(web3NetworkIDLoaded(id))
- dispatch(loadMainAccount());
- })
- .catch((err) => {
- dispatch(web3Error(err));
- });
- };
-}
-
-const loadMainAccount = () => {
- return (dispatch, getState) => {
- web3.eth.getAccounts()
- .then(accounts => {
- dispatch(web3AccoutLoaded(accounts[0]));
- })
- .catch((err) => {
- dispatch(web3Error(err));
- });
- };
-}
diff --git a/app/js/actions/web3.ts b/app/js/actions/web3.ts
new file mode 100644
index 0000000..ae6701b
--- /dev/null
+++ b/app/js/actions/web3.ts
@@ -0,0 +1,117 @@
+import Web3 from 'web3';
+import { config } from '../config';
+import {
+ Dispatch,
+} from 'redux';
+import { RootState } from '../reducers';
+
+export const VALID_NETWORK_NAME = "Ropsten";
+export const VALID_NETWORK_ID = 3;
+// export const VALID_NETWORK_NAME = "Goerli";
+// export const VALID_NETWORK_ID = 5;
+export const LOCAL_NETWORK_ID = 1337;
+
+enum Web3Type {
+ Generic,
+ Remote,
+ Status,
+}
+
+export const WEB3_INITIALIZED = "WEB3_INITIALIZED";
+export interface Web3InitializedAction {
+ type: typeof WEB3_INITIALIZED
+ web3Type: Web3Type
+}
+
+export const WEB3_ERROR = "WEB3_ERROR";
+export interface Web3ErrorAction {
+ type: typeof WEB3_ERROR
+ error: string
+}
+
+export const WEB3_NETWORK_ID_LOADED = "WEB3_NETWORK_ID_LOADED";
+export interface Web3NetworkIDLoadedAction {
+ type: typeof WEB3_NETWORK_ID_LOADED
+ networkID: number
+}
+
+export const WEB3_ACCOUNT_LOADED = "WEB3_ACCOUNT_LOADED";
+export interface Web3AccountLoadedAction {
+ type: typeof WEB3_ACCOUNT_LOADED
+ account: string
+}
+
+export type Web3Actions =
+ Web3InitializedAction |
+ Web3ErrorAction |
+ Web3NetworkIDLoadedAction |
+ Web3AccountLoadedAction;
+
+
+export const web3Initialized = (t: Web3Type): Web3Actions => ({
+ type: WEB3_INITIALIZED,
+ web3Type: t,
+})
+
+export const web3NetworkIDLoaded = (id: number): Web3Actions => ({
+ type: WEB3_NETWORK_ID_LOADED,
+ networkID: id,
+});
+
+export const web3Error = (error: string): Web3Actions => ({
+ type: WEB3_ERROR,
+ error: error,
+});
+
+export const accountLoaded = (account: string): Web3Actions => ({
+ type: WEB3_ACCOUNT_LOADED,
+ account
+});
+
+export const initializeWeb3 = () => {
+ const w = window as any;
+ return (dispatch: Dispatch, getState: () => RootState) => {
+ if (w.ethereum) {
+ config.web3 = new Web3(w.ethereum);
+ w.ethereum.enable()
+ .then(() => {
+ const t: Web3Type = w.ethereum.isStatus ? Web3Type.Status : Web3Type.Generic;
+ dispatch(web3Initialized(t));
+ config.web3!.eth.net.getId().then((id: number) => {
+ if (id !== VALID_NETWORK_ID && id !== LOCAL_NETWORK_ID) {
+ dispatch(web3Error(`wrong network, please connect to ${VALID_NETWORK_NAME}`));
+ return;
+ }
+
+ dispatch(web3NetworkIDLoaded(id))
+ dispatch(loadAddress());
+ });
+ })
+ .catch((err: string) => {
+ //FIXME: handle error
+ console.log("error", err)
+ });
+ } else if (config.web3) {
+ const t: Web3Type = w.ethereum.isStatus ? Web3Type.Status : Web3Type.Generic;
+ dispatch(web3Initialized(t));
+ config.web3!.eth.net.getId().then((id: number) => {
+ dispatch(web3NetworkIDLoaded(id))
+ dispatch(loadAddress());
+ })
+ .catch((err: string) => {
+ //FIXME: handle error
+ console.log("error", err)
+ });
+ } else {
+ dispatch(web3Error("web3 not supported"));
+ }
+ };
+}
+
+const loadAddress = () => {
+ return (dispatch: Dispatch, getState: () => RootState) => {
+ web3.eth.getAccounts().then((accounts: string[]) => {
+ dispatch(accountLoaded(accounts[0]));
+ });
+ };
+}
diff --git a/app/js/components/App.js b/app/js/components/App.js
deleted file mode 100644
index c015409..0000000
--- a/app/js/components/App.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-export default function App(props) {
- if (!props.initialized) {
- return "initializing...";
- }
-
- if (props.error) {
- return <>
-
Error: {props.error}
- >;
- }
-
- return <>
- Network ID {props.networkID}
- Hello {props.account}
- >;
-}
-
diff --git a/app/js/components/App.tsx b/app/js/components/App.tsx
new file mode 100644
index 0000000..ee7c263
--- /dev/null
+++ b/app/js/components/App.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import {
+ shallowEqual,
+ useSelector,
+ useDispatch,
+} from 'react-redux';
+
+export default function(ownProps: any) {
+ const props = useSelector(state => {
+ return {
+ initialized: state.web3.networkID,
+ networkID: state.web3.networkID,
+ error: state.web3.error,
+ }
+ }, shallowEqual);
+
+ if (props.error) {
+ return `Error: ${props.error}`;
+ }
+
+ if (!props.initialized) {
+ return "initializing...";
+ }
+
+ return ownProps.children;
+}
diff --git a/app/js/components/ErrorBoundary.tsx b/app/js/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..4186e4f
--- /dev/null
+++ b/app/js/components/ErrorBoundary.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error(error);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return
+
Something went wrong.
+
;
+ }
+
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
diff --git a/app/js/components/Home.tsx b/app/js/components/Home.tsx
new file mode 100644
index 0000000..e3c3211
--- /dev/null
+++ b/app/js/components/Home.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import {
+ useDispatch,
+} from 'react-redux';
+
+export default function() {
+ const dispatch = useDispatch();
+
+ return <>
+
+
+ >;
+}
diff --git a/app/js/components/Redeem.tsx b/app/js/components/Redeem.tsx
new file mode 100644
index 0000000..f9a2407
--- /dev/null
+++ b/app/js/components/Redeem.tsx
@@ -0,0 +1,88 @@
+import React, { useEffect } from 'react';
+import { useRouteMatch } from 'react-router-dom';
+import {
+ shallowEqual,
+ useSelector,
+ useDispatch,
+} from 'react-redux';
+import { redeemPath } from '../config';
+import {
+ loadGift,
+ BucketError,
+ ERROR_LOADING_GIFT,
+ ERROR_GIFT_NOT_FOUND,
+} from '../actions/bucket';
+import { toBaseUnit } from "../utils";
+
+const errorMessage = (error: BucketError): string => {
+ switch (error.type) {
+ case ERROR_LOADING_GIFT:
+ return "couldn't load gift.";
+
+ case ERROR_GIFT_NOT_FOUND:
+ return "gift not found";
+
+ default:
+ return "something went wrong";
+ }
+}
+
+export default function(ownProps: any) {
+ const dispatch = useDispatch()
+ const match = useRouteMatch({
+ path: redeemPath,
+ exact: true,
+ });
+
+ const bucketAddress = match.params.bucketAddress;
+ const recipientAddress = match.params.recipientAddress;
+
+ const props = useSelector(state => {
+ return {
+ bucketAddress: state.bucket.address,
+ loading: state.bucket.loading,
+ found: state.bucket.found,
+ error: state.bucket.error,
+ recipient: state.bucket.recipient,
+ amount: state.bucket.amount,
+ codeHash: state.bucket.codeHash,
+ tokenAddress: state.bucket.tokenAddress,
+ tokenSymbol: state.bucket.tokenSymbol,
+ tokenDecimals: state.bucket.tokenDecimals,
+ receiver: state.web3.account,
+ }
+ }, shallowEqual);
+
+ useEffect(() => {
+ dispatch(loadGift(bucketAddress, recipientAddress));
+ }, [bucketAddress, recipientAddress]);
+
+ if (props.error) {
+ return `Error: ${errorMessage(props.error)}`;
+ }
+
+ if (props.loading) {
+ return "loading bucket...";
+ }
+
+ if (props.tokenSymbol === undefined || props.tokenDecimals === undefined) {
+ return "loading token info...";
+ }
+
+ const [displayAmount, roundedDisplayAmount] = toBaseUnit(props.amount, props.tokenDecimals, 2);
+
+ return <>
+ Bucket Address: {props.bucketAddress}
+ Recipient: {props.recipient}
+ Amount: {props.amount}
+ Code Hash: {props.codeHash}
+ Token Address: {props.tokenAddress}
+ Token Symbol: {props.tokenSymbol}
+ Token Decimals: {props.tokenDecimals}
+ Display Amount: {displayAmount}
+ Rounded Display Amount: {roundedDisplayAmount}
+ Receiver: {props.receiver}
+
+
+ >;
+}
diff --git a/app/js/config.js b/app/js/config.js
deleted file mode 100644
index 44614d8..0000000
--- a/app/js/config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const config = {
- web3: undefined,
-};
diff --git a/app/js/config.ts b/app/js/config.ts
new file mode 100644
index 0000000..60ebf0c
--- /dev/null
+++ b/app/js/config.ts
@@ -0,0 +1,7 @@
+import Web3 from "web3";
+
+export const config = {
+ web3: Web3 | undefined
+};
+
+export const redeemPath = "/redeem/:bucketAddress/:recipientAddress";
diff --git a/app/js/containers/App.js b/app/js/containers/App.js
deleted file mode 100644
index f4b7ae4..0000000
--- a/app/js/containers/App.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { connect } from 'react-redux';
-import App from '../components/App';
-
-const mapStateToProps = state => ({
- initialized: state.web3.networkID,
- networkID: state.web3.networkID,
- account: state.web3.account,
- error: state.web3.error,
-});
-
-const mapDispatchToProps = dispatch => ({
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(App);
diff --git a/app/js/index.js b/app/js/index.js
deleted file mode 100644
index 6dda51d..0000000
--- a/app/js/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import EmbarkJS from 'Embark/EmbarkJS';
-import React, { useEffect } from 'react';
-import ReactDOM from 'react-dom';
-import thunkMiddleware from 'redux-thunk';
-import { Provider } from 'react-redux';
-import { createStore, applyMiddleware } from 'redux';
-import createRootReducer from './reducers';
-import { initWeb3 } from './actions/web3';
-import App from './containers/App';
-
-const logger = (store) => {
- return (next) => {
- return (action) => {
- console.log('dispatching\n', action);
- const result = next(action);
- console.log('next state\n', store.getState());
- return result;
- }
- }
-};
-
-let middlewares = [
- thunkMiddleware,
-];
-
-if (true || process.env.NODE_ENV !== 'production') {
- middlewares = [
- ...middlewares,
- logger
- ];
-}
-
-const store = createStore(
- createRootReducer(),
- applyMiddleware(...middlewares),
-);
-
-EmbarkJS.onReady((err) => {
- store.dispatch(initWeb3());
-
- ReactDOM.render(
-
-
- ,
- document.getElementById("root")
- );
-});
diff --git a/app/js/index.tsx b/app/js/index.tsx
new file mode 100644
index 0000000..a5bb198
--- /dev/null
+++ b/app/js/index.tsx
@@ -0,0 +1,63 @@
+import EmbarkJS from 'Embark/EmbarkJS';
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import thunkMiddleware from 'redux-thunk';
+import { Provider } from 'react-redux';
+import { createStore, applyMiddleware, Middleware, MiddlewareAPI, Dispatch } from 'redux';
+import createRootReducer from './reducers';
+import { initializeWeb3 } from './actions/web3';
+import { routerMiddleware, ConnectedRouter } from 'connected-react-router';
+import { Route, Switch } from 'react-router';
+import { createHashHistory } from 'history';
+import ErrorBoundary from './components/ErrorBoundary';
+import App from './components/App';
+import Home from './components/Home';
+import Redeem from './components/Redeem';
+import { redeemPath } from './config';
+
+const logger: Middleware = ({ getState }: MiddlewareAPI) => (next: Dispatch) => action => {
+ console.log('will dispatch', action);
+ const returnValue = next(action);
+ console.log('state after dispatch', getState());
+ return returnValue;
+}
+
+const history = createHashHistory();
+
+let middlewares: Middleware[] = [
+ routerMiddleware(history),
+ thunkMiddleware,
+];
+
+if (true || process.env.NODE_ENV !== 'production') {
+ middlewares = [
+ ...middlewares,
+ logger
+ ];
+}
+
+const store = createStore(
+ createRootReducer(history),
+ applyMiddleware(...middlewares),
+);
+
+EmbarkJS.onReady(err => {
+ store.dispatch(initializeWeb3());
+
+ ReactDOM.render(
+
+
+
+
+
+
+
+ "page not found"} />
+
+
+
+
+ ,
+ document.getElementById("root")
+ );
+});
diff --git a/app/js/reducers/bucket.ts b/app/js/reducers/bucket.ts
new file mode 100644
index 0000000..3aa1872
--- /dev/null
+++ b/app/js/reducers/bucket.ts
@@ -0,0 +1,79 @@
+import {
+ BucketActions,
+ BucketError,
+ BUCKET_GIFT_LOADING,
+ BUCKET_GIFT_NOT_FOUND,
+ BUCKET_GIFT_LOADED,
+ BUCKET_TOKEN_LOADING,
+ BUCKET_TOKEN_LOADED,
+} from "../actions/bucket";
+
+export interface BucketState {
+ loading: boolean
+ address: string | undefined
+ tokenAddress: string | undefined
+ tokenDecimals: number | undefined
+ error: BucketState | undefined
+ recipient: string | undefined
+ amount: string | undefined
+ codeHash: string | undefined
+}
+
+const initialState: BucketState = {
+ loading: false,
+ address: undefined,
+ tokenAddress: undefined,
+ tokenDecimals: undefined,
+ error: undefined,
+ recipient: undefined,
+ amount: undefined,
+ codeHash: undefined,
+}
+
+export const bucketReducer = (state: BucketState = initialState, action: BucketActions): BucketState => {
+ switch (action.type) {
+ case BUCKET_GIFT_LOADING: {
+ return {
+ ...initialState,
+ loading: true,
+ address: action.address,
+ }
+ }
+
+ case BUCKET_GIFT_NOT_FOUND: {
+ return {
+ ...state,
+ loading: false,
+ error: action.error,
+ }
+ }
+
+ case BUCKET_GIFT_LOADED: {
+ return {
+ ...state,
+ loading: false,
+ recipient: action.recipient,
+ amount: action.amount,
+ codeHash: action.codeHash,
+ }
+ }
+
+ case BUCKET_TOKEN_LOADING: {
+ return {
+ ...state,
+ tokenAddress: action.address,
+ }
+ }
+
+ case BUCKET_TOKEN_LOADED: {
+ return {
+ ...state,
+ tokenSymbol: action.symbol,
+ tokenDecimals: action.decimals,
+ }
+ }
+
+ default:
+ return state
+ }
+}
diff --git a/app/js/reducers/index.js b/app/js/reducers/index.js
deleted file mode 100644
index dcfd65e..0000000
--- a/app/js/reducers/index.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { combineReducers } from 'redux';
-import { web3Reducer } from './web3';
-
-export default function() {
- return combineReducers({
- web3: web3Reducer,
- });
-}
diff --git a/app/js/reducers/index.ts b/app/js/reducers/index.ts
new file mode 100644
index 0000000..cb186c8
--- /dev/null
+++ b/app/js/reducers/index.ts
@@ -0,0 +1,23 @@
+import { combineReducers } from 'redux';
+import { connectRouter } from 'connected-react-router';
+import {
+ Web3State,
+ web3Reducer,
+} from './web3';
+import {
+ BucketState,
+ bucketReducer,
+} from './bucket';
+
+export interface RootState {
+ web3: Web3State,
+ bucket: BucketState,
+}
+
+export default function(history) {
+ return combineReducers({
+ web3: web3Reducer,
+ router: connectRouter(history),
+ bucket: bucketReducer,
+ });
+}
diff --git a/app/js/reducers/web3.js b/app/js/reducers/web3.ts
similarity index 71%
rename from app/js/reducers/web3.js
rename to app/js/reducers/web3.ts
index dde7664..8dc73ab 100644
--- a/app/js/reducers/web3.js
+++ b/app/js/reducers/web3.ts
@@ -1,10 +1,18 @@
import {
+ Web3Actions,
WEB3_INITIALIZED,
WEB3_ERROR,
WEB3_NETWORK_ID_LOADED,
WEB3_ACCOUNT_LOADED,
} from '../actions/web3';
+export interface Web3State {
+ initialized: boolean
+ networkID: number | undefined
+ error: string | undefined
+ account: string | undefined
+}
+
const initialState: Web3State = {
initialized: false,
networkID: undefined,
@@ -12,7 +20,7 @@ const initialState: Web3State = {
account: undefined,
};
-export const web3Reducer = (state = initialState, action) => {
+export const web3Reducer = (state: Web3State = initialState, action: Web3Actions): Web3State => {
switch (action.type) {
case WEB3_INITIALIZED: {
return {
@@ -41,7 +49,8 @@ export const web3Reducer = (state = initialState, action) => {
account: action.account,
}
}
- }
- return state;
+ default:
+ return state
+ }
}
diff --git a/app/js/utils.ts b/app/js/utils.ts
new file mode 100644
index 0000000..73e0251
--- /dev/null
+++ b/app/js/utils.ts
@@ -0,0 +1,18 @@
+import Web3Utils from "web3-utils";
+
+const BN = Web3Utils.BN;
+
+export const toBaseUnit = (fullAmount: string, decimalsSize: number, roundDecimals: number) => {
+ const amount = new BN(fullAmount);
+ const base = new BN(10).pow(new BN(decimalsSize));
+ const whole = amount.div(base).toString();
+ let decimals = amount.mod(base).toString();
+ for (let i = decimals.length; i < decimalsSize; i++) {
+ decimals = `0${decimals}`;
+ }
+
+ const full = `${whole}.${decimals}`;
+ const rounded = `${whole}.${decimals.slice(0, roundDecimals)}`;
+
+ return [full, rounded];
+}
diff --git a/config/contracts.js b/config/contracts.js
index 9b06ad0..b036281 100644
--- a/config/contracts.js
+++ b/config/contracts.js
@@ -3,8 +3,8 @@ module.exports = {
default: {
// order of connections the dapp should connect to
dappConnection: [
- "$EMBARK",
"$WEB3", // uses pre existing web3 object if available (e.g in Mist)
+ "$EMBARK",
"ws://localhost:8546",
"http://localhost:8545"
],
@@ -22,7 +22,7 @@ module.exports = {
// when not specified
// - explicit will only attempt to deploy the contracts that are explicitly specified inside the
// contracts section.
- // strategy: 'implicit',
+ strategy: 'explicit',
// minimalContractSize, when set to true, tells Embark to generate contract files without the heavy bytecodes
// Using filteredFields lets you customize which field you want to filter out of the contract file (requires minimalContractSize: true)
@@ -30,9 +30,12 @@ module.exports = {
// filteredFields: [],
deploy: {
- GiftBucket: {
- deploy: false,
- }
+ TestToken: {
+ args: ["TEST", 18],
+ },
+ GiftBucketFactory: {
+ params: [],
+ },
}
},
diff --git a/contracts/ERC20Token.sol b/contracts/ERC20Token.sol
deleted file mode 100644
index c4ecd76..0000000
--- a/contracts/ERC20Token.sol
+++ /dev/null
@@ -1,15 +0,0 @@
-pragma solidity ^0.6.1;
-
-// https://github.com/ethereum/EIPs/issues/20
-
-interface ERC20Token {
- event Transfer(address indexed _from, address indexed _to, uint256 _value);
- event Approval(address indexed _owner, address indexed _spender, uint256 _value);
-
- function transfer(address _to, uint256 _value) external returns (bool success);
- function approve(address _spender, uint256 _value) external returns (bool success);
- function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
- function balanceOf(address _owner) external view returns (uint256 balance);
- function allowance(address _owner, address _spender) external view returns (uint256 remaining);
- function totalSupply() external view returns (uint256 supply);
-}
diff --git a/contracts/GiftBucket.sol b/contracts/GiftBucket.sol
index 0039766..9eb484b 100644
--- a/contracts/GiftBucket.sol
+++ b/contracts/GiftBucket.sol
@@ -1,7 +1,7 @@
pragma solidity ^0.6.1;
pragma experimental ABIEncoderV2;
-import "./ERC20Token.sol";
+import "./IERC20.sol";
contract GiftBucket {
@@ -9,7 +9,7 @@ contract GiftBucket {
address payable public owner;
- ERC20Token public tokenContract;
+ IERC20 public tokenContract;
uint256 public expirationTime;
@@ -46,7 +46,7 @@ contract GiftBucket {
require(_expirationTime > block.timestamp, "expiration can't be in the past");
- tokenContract = ERC20Token(_tokenAddress);
+ tokenContract = IERC20(_tokenAddress);
expirationTime = _expirationTime;
owner = payable(_owner);
diff --git a/contracts/GiftBucketFactory.sol b/contracts/GiftBucketFactory.sol
index af779dc..399bb69 100644
--- a/contracts/GiftBucketFactory.sol
+++ b/contracts/GiftBucketFactory.sol
@@ -13,7 +13,6 @@ contract GiftBucketFactory {
}
function create(address _tokenAddress, uint256 _expirationTime) public returns (address) {
- // initialize(address,uint256,address)
address p = address(new Proxy(abi.encodeWithSelector(0xc350a1b5, _tokenAddress, _expirationTime, msg.sender), address(GiftBucketImplementation)));
emit Created(msg.sender, p);
return p;
diff --git a/contracts/IERC20.sol b/contracts/IERC20.sol
new file mode 100644
index 0000000..3fb727d
--- /dev/null
+++ b/contracts/IERC20.sol
@@ -0,0 +1,15 @@
+pragma solidity ^0.6.1;
+
+// https://github.com/ethereum/EIPs/issues/20
+
+interface IERC20 {
+ event Transfer(address indexed _from, address indexed _to, uint256 _value);
+ event Approval(address indexed _owner, address indexed _spender, uint256 _value);
+
+ function transfer(address _to, uint256 _value) external returns (bool success);
+ function approve(address _spender, uint256 _value) external returns (bool success);
+ function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
+ function balanceOf(address _owner) external view returns (uint256 balance);
+ function allowance(address _owner, address _spender) external view returns (uint256 remaining);
+ function totalSupply() external view returns (uint256 supply);
+}
diff --git a/contracts/IERC20Detailed.sol b/contracts/IERC20Detailed.sol
new file mode 100644
index 0000000..d76f52e
--- /dev/null
+++ b/contracts/IERC20Detailed.sol
@@ -0,0 +1,9 @@
+pragma solidity >=0.5.0 <0.7.0;
+
+import "./IERC20.sol";
+
+abstract contract IERC20Detailed is IERC20 {
+ function symbol() virtual public view returns (string memory);
+ function decimals() virtual public view returns (uint8);
+}
+
diff --git a/contracts/StandardToken.sol b/contracts/StandardToken.sol
index c23967d..9c15bcf 100644
--- a/contracts/StandardToken.sol
+++ b/contracts/StandardToken.sol
@@ -1,8 +1,8 @@
pragma solidity ^0.6.1;
-import "./ERC20Token.sol";
+import "./IERC20.sol";
-contract StandardToken is ERC20Token {
+contract StandardToken is IERC20 {
uint256 private supply;
mapping (address => uint256) balances;
@@ -15,7 +15,7 @@ contract StandardToken is ERC20Token {
uint256 _value
)
external
- override(ERC20Token)
+ override(IERC20)
returns (bool success)
{
return transfer(msg.sender, _to, _value);
@@ -23,7 +23,7 @@ contract StandardToken is ERC20Token {
function approve(address _spender, uint256 _value)
external
- override(ERC20Token)
+ override(IERC20)
returns (bool success)
{
allowed[msg.sender][_spender] = _value;
@@ -37,7 +37,7 @@ contract StandardToken is ERC20Token {
uint256 _value
)
external
- override(ERC20Token)
+ override(IERC20)
returns (bool success)
{
if (balances[_from] >= _value &&
@@ -53,7 +53,7 @@ contract StandardToken is ERC20Token {
function allowance(address _owner, address _spender)
external
view
- override(ERC20Token)
+ override(IERC20)
returns (uint256 remaining)
{
return allowed[_owner][_spender];
@@ -62,7 +62,7 @@ contract StandardToken is ERC20Token {
function balanceOf(address _owner)
external
view
- override(ERC20Token)
+ override(IERC20)
returns (uint256 balance)
{
return balances[_owner];
@@ -71,7 +71,7 @@ contract StandardToken is ERC20Token {
function totalSupply()
external
view
- override(ERC20Token)
+ override(IERC20)
returns(uint256 currentTotalSupply)
{
return supply;
diff --git a/contracts/TestToken.sol b/contracts/TestToken.sol
index cf2f655..d0056d8 100644
--- a/contracts/TestToken.sol
+++ b/contracts/TestToken.sol
@@ -6,14 +6,32 @@ import "./StandardToken.sol";
* @notice ERC20Token for test scripts, can be minted by anyone.
*/
contract TestToken is StandardToken {
+ string private _symbol;
+ uint256 private _decimals;
- constructor() public { }
+ constructor(string memory symbol, uint256 decimals) public {
+ _symbol = symbol;
+ _decimals = decimals;
+ }
- /**
- * @notice any caller can mint any `_amount`
- * @param _amount how much to be minted
- */
- function mint(uint256 _amount) public {
- mint(msg.sender, _amount);
- }
+ fallback() external {
+ uint256 amount = 5000;
+ mint(amount * uint256(10)**_decimals);
+ }
+
+ function symbol() public view returns (string memory) {
+ return _symbol;
+ }
+
+ function decimals() public view returns (uint256) {
+ return _decimals;
+ }
+
+ /**
+ * @notice any caller can mint any `_amount`
+ * @param _amount how much to be minted
+ */
+ function mint(uint256 _amount) public {
+ mint(msg.sender, _amount);
+ }
}
diff --git a/embark.json b/embark.json
index 19d9028..2dedc20 100644
--- a/embark.json
+++ b/embark.json
@@ -2,7 +2,7 @@
"contracts": ["contracts/**"],
"app": {
"css/app.css": ["app/css/**"],
- "js/app.js": ["app/js/index.js"],
+ "js/app.js": ["app/js/index.tsx"],
"images/": ["app/images/**"],
"index.html": "app/index.html"
},
@@ -12,6 +12,13 @@
"solc": "0.6.1"
},
"plugins": {
+ "embark-ipfs": {},
+ "embark-swarm": {},
+ "embark-whisper-geth": {},
+ "embark-geth": {},
+ "embark-parity": {},
+ "embark-profiler": {},
+ "embark-graph": {}
},
"options": {
"solc": {
diff --git a/package.json b/package.json
index 490cf0e..c239026 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"react-dom": "^16.12.0",
"react-redux": "^7.2.0",
"react-router": "^5.1.2",
+ "react-router-dom": "^5.1.2",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"web3": "^1.2.6"
diff --git a/test/contract_spec.js b/test/contract_spec.js
index ea33886..4748bd4 100644
--- a/test/contract_spec.js
+++ b/test/contract_spec.js
@@ -20,7 +20,7 @@ config({
contracts: {
deploy: {
"TestToken": {
- args: [],
+ args: ["TEST", 18],
},
"GiftBucket": {
args: ["$TestToken", EXPIRATION_TIME],
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f2850b7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..180b0a7
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,188 @@
+// some packages, plugins, and presets referenced/required in this webpack
+// config are deps of embark and will be transitive dapp deps unless specified
+// in the dapp's own package.json
+
+// embark modifies process.env.NODE_PATH so that when running dapp scripts in
+// embark's child processes, embark's own node_modules directory will be
+// searched by node's require(); however, webpack and babel do not directly
+// support NODE_PATH, so modules such as babel plugins and presets must be
+// resolved with require.resolve(); that is only necessary if a plugin/preset
+// is in embark's node_modules vs. the dapp's node_modules
+
+const cloneDeep = require('lodash.clonedeep');
+// const CompressionPlugin = require('compression-webpack-plugin');
+const glob = require('glob');
+const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
+const path = require('path');
+
+const dappPath = process.env.DAPP_PATH;
+const embarkPath = process.env.EMBARK_PATH;
+
+const embarkAliases = require(path.join(dappPath, '.embark/embark-aliases.json'));
+const embarkAssets = require(path.join(dappPath, '.embark/embark-assets.json'));
+const embarkNodeModules = path.join(embarkPath, 'node_modules');
+const embarkJson = require(path.join(dappPath, 'embark.json'));
+
+const buildDir = path.join(dappPath, embarkJson.buildDir);
+
+// it's important to `embark reset` if a pkg version is specified in
+// embark.json and changed/removed later, otherwise pkg resolution may behave
+// unexpectedly
+let versions;
+try {
+ versions = glob.sync(path.join(dappPath, '.embark/versions/*/*'));
+} catch (e) {
+ versions = [];
+}
+
+const entry = Object.keys(embarkAssets)
+ .filter(key => key.match(/\.js$/))
+ .reduce((obj, key) => {
+ // webpack entry paths should start with './' if they're relative to the
+ // webpack context; embark.json "app" keys correspond to lists of .js
+ // source paths relative to the top-level dapp dir and may be missing the
+ // leading './'
+ obj[key] = embarkAssets[key]
+ .map(file => {
+ let file_path = file.path;
+ if (!file.path.match(/^\.\//)) {
+ file_path = './' + file_path;
+ }
+ return file_path;
+ });
+ return obj;
+ }, {});
+
+function resolve(pkgName) {
+ if (Array.isArray(pkgName)) {
+ const _pkgName = pkgName[0];
+ pkgName[0] = require.resolve(_pkgName);
+ return pkgName;
+ }
+ return require.resolve(pkgName);
+}
+
+// base config
+// -----------------------------------------------------------------------------
+
+const base = {
+ context: dappPath,
+ entry: entry,
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: [{loader: 'style-loader'}, {loader: 'css-loader'}]
+ },
+ {
+ test: /\.scss$/,
+ use: [{loader: 'style-loader'}, {loader: 'css-loader'}]
+ },
+ {
+ test: /\.(png|woff|woff2|eot|ttf|svg)$/,
+ loader: 'url-loader?limit=100000'
+ },
+ {
+ test: /\.(js|jsx|tsx|ts)$/,
+ loader: 'babel-loader',
+ exclude: /(node_modules|bower_components|\.embark[\\/]versions)/,
+ options: {
+ plugins: [
+ [
+ 'babel-plugin-module-resolver', {
+ 'alias': embarkAliases
+ }
+ ],
+ [
+ '@babel/plugin-transform-runtime', {
+ corejs: 2,
+ useESModules: true
+ }
+ ]
+ ].map(resolve),
+ presets: [
+ [
+ '@babel/preset-env', {
+ modules: false,
+ targets: {
+ browsers: ['last 1 version', 'not dead', '> 0.2%']
+ }
+ }
+ ],
+ '@babel/preset-react',
+ '@babel/preset-typescript'
+ ].map(resolve)
+ }
+ }
+ ]
+ },
+ output: {
+ filename: (chunkData) => chunkData.chunk.name,
+ // globalObject workaround for node-compatible UMD builds with webpack 4
+ // see: https://github.com/webpack/webpack/issues/6522#issuecomment-371120689
+ globalObject: 'typeof self !== \'undefined\' ? self : this',
+ libraryTarget: 'umd',
+ path: buildDir
+ },
+ plugins: [new HardSourceWebpackPlugin()],
+ // profiling and generating verbose stats increases build time; if stats
+ // are generated embark will write the output to:
+ // path.join(dappPath, '.embark/stats.[json,report]')
+ // to visualize the stats info in a browser run:
+ // npx webpack-bundle-analyzer .embark/stats.json
+ profile: true, stats: 'verbose',
+ resolve: {
+ alias: embarkAliases,
+ modules: [
+ ...versions,
+ 'node_modules',
+ embarkNodeModules
+ ]
+ },
+ resolveLoader: {
+ modules: [
+ 'node_modules',
+ embarkNodeModules
+ ]
+ }
+};
+
+// typescript mods
+// -----------------------------------------------------------------------------
+base.resolve.extensions = [
+ // webpack defaults
+ // see: https://webpack.js.org/configuration/resolve/#resolve-extensions
+ '.wasm', '.mjs', '.js', '.json',
+ // typescript extensions
+ '.ts', '.tsx'
+];
+
+// development config
+// -----------------------------------------------------------------------------
+
+const development = cloneDeep(base);
+// full source maps increase build time but are useful during dapp development
+development.devtool = 'source-map';
+development.mode = 'development';
+// alternatively:
+// development.mode = 'none';
+development.name = 'development';
+const devBabelLoader = development.module.rules[3];
+devBabelLoader.options.compact = false;
+
+// production config
+// -----------------------------------------------------------------------------
+
+const production = cloneDeep(base);
+production.mode = 'production';
+production.name = 'production';
+// compression of webpack's JS output not enabled by default
+// production.plugins.push(new CompressionPlugin());
+
+// export a list of named configs
+// -----------------------------------------------------------------------------
+
+module.exports = [
+ development,
+ production
+];
diff --git a/yarn.lock b/yarn.lock
index 756f010..2b0f2bf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9628,7 +9628,20 @@ react-redux@^7.2.0:
prop-types "^15.7.2"
react-is "^16.9.0"
-react-router@^5.1.2:
+react-router-dom@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
+ integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ history "^4.9.0"
+ loose-envify "^1.3.1"
+ prop-types "^15.6.2"
+ react-router "5.1.2"
+ tiny-invariant "^1.0.2"
+ tiny-warning "^1.0.0"
+
+react-router@5.1.2, react-router@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==