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==