diff --git a/client/package.json b/client/package.json index da5617e..861c04c 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "@types/react-fontawesome": "^1.6.4", "@types/react-redux": "^7.1.7", "@types/react-router-dom": "^5.1.5", + "@types/react-router-redux": "^5.0.18", "bn.js": "^5.1.1", "classnames": "^2.2.6", "connected-react-router": "^6.7.0", @@ -34,6 +35,7 @@ "react-redux": "^7.2.0", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", + "react-router-redux": "^4.0.8", "react-scripts": "3.4.1", "redux": "^4.0.5", "redux-thunk": "^2.3.0", diff --git a/client/src/actions/home.ts b/client/src/actions/home.ts new file mode 100644 index 0000000..749c11a --- /dev/null +++ b/client/src/actions/home.ts @@ -0,0 +1,28 @@ +import { Dispatch } from 'redux'; +import { RootState } from '../reducers'; +import { Web3Type } from "../actions/web3"; +import { + SignRedeemResponse, + signTypedDataWithKeycard, + signTypedDataWithWeb3, + signTypedLogin, +} from "../utils"; +import { buildRecipientBucketsPath } from "../config"; +import { push } from "react-router-redux"; + +export const start = () => { + return (dispatch: Dispatch, getState: () => RootState) => { + const state = getState(); + const account = state.web3.account!; + const web3Type = state.web3.type; + const chainID = state.web3.chainID!; + + signTypedLogin(chainID, account, web3Type).then((resp: SignRedeemResponse) => { + const path = buildRecipientBucketsPath(resp.signer); + dispatch(push(path)); + }).catch(err => { + //FIXME: handle error + console.error(err) + }); + } +} diff --git a/client/src/actions/layout.ts b/client/src/actions/layout.ts index 0b9b28a..7f0fc00 100644 --- a/client/src/actions/layout.ts +++ b/client/src/actions/layout.ts @@ -1,9 +1,3 @@ -export const LAYOUT_TOGGLE_SIDEBAR = "LAYOUT_TOGGLE_SIDEBAR"; -export interface LayoutToggleSidebarAction { - type: typeof LAYOUT_TOGGLE_SIDEBAR - open: boolean -} - export const LAYOUT_FLIP_CARD = "LAYOUT_FLIP_CARD"; export interface LayoutFlipCardAction { type: typeof LAYOUT_FLIP_CARD @@ -17,15 +11,9 @@ export interface LayoutToggleDebugAction { } export type LayoutActions = - LayoutToggleSidebarAction | LayoutFlipCardAction | LayoutToggleDebugAction; -export const toggleSidebar = (open: boolean): LayoutToggleSidebarAction => ({ - type: LAYOUT_TOGGLE_SIDEBAR, - open, -}); - export const toggleDebug = (open: boolean): LayoutToggleDebugAction => ({ type: LAYOUT_TOGGLE_DEBUG, open, diff --git a/client/src/actions/redeem.ts b/client/src/actions/redeem.ts index 889ffdc..01e2282 100644 --- a/client/src/actions/redeem.ts +++ b/client/src/actions/redeem.ts @@ -2,9 +2,14 @@ import { RootState } from '../reducers'; import { config } from "../config"; import { Dispatch } from 'redux'; import { sha3 } from "web3-utils"; -import { recoverTypedSignature } from 'eth-sig-util'; import { Web3Type } from "../actions/web3"; -import { KECCAK_EMPTY_STRING, newBucketContract} from '../utils'; +import { + KECCAK_EMPTY_STRING, + newBucketContract, + SignRedeemResponse, + signTypedDataWithKeycard, + signTypedDataWithWeb3, +} from '../utils'; import { debug } from "./debug"; interface RedeemMessage { @@ -14,11 +19,6 @@ interface RedeemMessage { blockHash: string } -interface SignRedeemResponse { - sig: string - signer: string -} - export const ERROR_REDEEMING = "ERROR_REDEEMING"; export interface ErrRedeeming { type: typeof ERROR_REDEEMING @@ -96,6 +96,7 @@ export const redeem = (bucketAddress: string, recipientAddress: string, cleanCod dispatch(redeeming()); const state = getState(); + const chainID = state.web3.chainID!; const web3Type = state.web3.type; const bucket = newBucketContract(bucketAddress); @@ -118,7 +119,7 @@ export const redeem = (bucketAddress: string, recipientAddress: string, cleanCod const domainName = isERC20 ? "KeycardERC20Bucket" : "KeycardNFTBucket"; //FIXME: is signer needed? dispatch(debug("signing redeem")); - signRedeem(web3Type, bucketAddress, state.web3.account!, message, domainName).then(async ({ sig, signer }: SignRedeemResponse) => { + signRedeem(chainID, web3Type, bucketAddress, state.web3.account!, message, domainName).then(async ({ sig, signer }: SignRedeemResponse) => { dispatch(debug(`signature: ${sig}, signer: ${signer}`)); const recipient = state.redeemable.recipient!; if (signer.toLowerCase() !== recipient.toLowerCase()) { @@ -136,9 +137,7 @@ export const redeem = (bucketAddress: string, recipientAddress: string, cleanCod } } -async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: string, message: RedeemMessage, domainName: string): Promise { - const chainId = await config.web3!.eth.net.getId(); - +async function signRedeem(chainID: number, web3Type: Web3Type, contractAddress: string, signer: string, message: RedeemMessage, domainName: string): Promise { const domain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, @@ -156,7 +155,7 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s const domainData = { name: domainName, version: "1", - chainId: chainId, + chainId: chainID, verifyingContract: contractAddress }; @@ -171,48 +170,12 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s }; if (web3Type === Web3Type.Status) { - return signWithKeycard(data); + return signTypedDataWithKeycard(data); } else { - return signWithWeb3(signer, data); + return signTypedDataWithWeb3(signer, data); } } -const signWithWeb3 = (signer: string, data: any): Promise => { - return new Promise((resolve, reject) => { - (window as any).ethereum.sendAsync({ - method: "eth_signTypedData_v3", - params: [signer, JSON.stringify(data)], - from: signer, - }, (err: string, resp: any) => { - if (err) { - reject(err); - } else { - const sig = resp.result; - const signer = recoverTypedSignature({ - data, - sig - }); - - resolve({ sig, signer }); - } - }) - }); -} - -const signWithKeycard = (data: any): Promise => { - return new Promise((resolve, reject) => { - (window as any).ethereum.send("keycard_signTypedData", JSON.stringify(data)).then((sig: any) => { - const signer = recoverTypedSignature({ - data, - sig - }); - resolve({ sig, signer }); - }).catch((err: string) => { - reject(err); - }) - }); -} - //FIXME: fix bucket contract type const sendTransaction = (account: string, bucket: any, bucketAddress: string, message: RedeemMessage, sig: string) => { return (dispatch: Dispatch, getState: () => RootState) => { diff --git a/client/src/actions/redeemable.ts b/client/src/actions/redeemable.ts index c712f25..474bc63 100644 --- a/client/src/actions/redeemable.ts +++ b/client/src/actions/redeemable.ts @@ -182,8 +182,9 @@ export const tokenMetadataLoaded = (tokenAddress: string, recipient: string, met export const loadRedeemable = (bucketAddress: string, recipientAddress: string) => { return async (dispatch: Dispatch, getState: () => RootState) => { const networkID = getState().web3.networkID!; - dispatch(debug(`erc20 factory address: ${contractAddress(ERC20BucketFactory, networkID)}`)); - dispatch(debug(`nft factory address: ${contractAddress(NFTBucketFactory, networkID)}`)); + // FIXME: how can we set the address if deployed via make + // dispatch(debug(`erc20 factory address: ${contractAddress(ERC20BucketFactory, networkID)}`)); + // dispatch(debug(`nft factory address: ${contractAddress(NFTBucketFactory, networkID)}`)); dispatch(debug(`bucket address: ${bucketAddress}`)); dispatch(debug(`recipient address: ${recipientAddress}`)); dispatch(loadingRedeemable(bucketAddress, recipientAddress)); diff --git a/client/src/actions/web3.ts b/client/src/actions/web3.ts index 31b0785..3a9583e 100644 --- a/client/src/actions/web3.ts +++ b/client/src/actions/web3.ts @@ -6,15 +6,17 @@ import { import { RootState } from '../reducers'; import { debug } from "./debug"; -export const VALID_NETWORK_NAME = "Ganache"; -export const VALID_NETWORK_ID = 5777; +// export const VALID_NETWORK_NAME = "Ganache"; +// export const VALID_NETWORK_ID = 5777; -// export const VALID_NETWORK_NAME = "Ropsten"; -// export const VALID_NETWORK_ID = 3; +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 LOCAL_NETWORK_IDS = [1337, 5777]; +export const VALID_NETWORK_IDS = [VALID_NETWORK_ID, ...LOCAL_NETWORK_IDS]; export enum Web3Type { None, @@ -39,6 +41,7 @@ export const WEB3_NETWORK_ID_LOADED = "WEB3_NETWORK_ID_LOADED"; export interface Web3NetworkIDLoadedAction { type: typeof WEB3_NETWORK_ID_LOADED networkID: number + chainID: number } export const WEB3_ACCOUNT_LOADED = "WEB3_ACCOUNT_LOADED"; @@ -59,9 +62,10 @@ export const web3Initialized = (t: Web3Type): Web3Actions => ({ web3Type: t, }) -export const web3NetworkIDLoaded = (id: number): Web3Actions => ({ +export const web3NetworkIDLoaded = (networkID: number, chainID: number): Web3Actions => ({ type: WEB3_NETWORK_ID_LOADED, - networkID: id, + networkID, + chainID, }); export const web3Error = (error: string): Web3Actions => ({ @@ -76,42 +80,25 @@ export const accountLoaded = (account: string): Web3Actions => ({ export const initializeWeb3 = () => { const w = window as any; - return (dispatch: Dispatch, getState: () => RootState) => { + return async (dispatch: Dispatch, getState: () => RootState) => { if (w.ethereum) { config.web3 = new Web3(w.ethereum); (config.web3! as any).eth.handleRevert = true; 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(debug(`network id: ${id}`)) - dispatch(web3NetworkIDLoaded(id)) + checkNetworkAndChainId().then((resp: any) => { + dispatch(debug(`network id: ${resp.networkID}`)) + dispatch(debug(`chain id: ${resp.chainID}`)) + dispatch(web3NetworkIDLoaded(resp.networkID, resp.chainID)) + dispatch(web3Initialized(resp.type)); dispatch(loadAddress()); - }); + }) }) .catch((err: string) => { - //FIXME: handle error - console.log("error", err) + console.error("web3 error", err) + dispatch(web3Error(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(debug(`network id: ${id}`)) - dispatch(web3NetworkIDLoaded(id)) - dispatch(loadAddress()); - }) - .catch((err: string) => { - //FIXME: handle error - console.log("error", err) - }); } else { dispatch(web3Error("web3 not supported")); } @@ -126,3 +113,36 @@ const loadAddress = () => { }); }; } + +const checkNetworkAndChainId = async () => { + return new Promise(async (resolve, reject) => { + try { + const networkID = await config.web3!.eth.net.getId(); + const type = (window as any).ethereum.isStatus ? Web3Type.Status : Web3Type.Generic; + + if (!VALID_NETWORK_IDS.includes(networkID)) { + reject(`wrong network, please connect to ${VALID_NETWORK_NAME}`); + return; + } + + let chainID; + + //FIXME: status should fix the getChainId error + if (type === Web3Type.Status) { + chainID = networkID; + } else if (LOCAL_NETWORK_IDS.includes(networkID)) { + chainID = 1; + } else { + chainID = await config.web3!.eth.getChainId(); + } + + resolve({ + type, + networkID, + chainID, + }); + } catch(e) { + reject(e); + } + }); +} diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx index b55ed76..a85adfd 100644 --- a/client/src/components/Home.tsx +++ b/client/src/components/Home.tsx @@ -2,8 +2,12 @@ import React from 'react'; import { useDispatch, } from 'react-redux'; +import { start } from "../actions/home"; export default function() { + const dispatch = useDispatch() + return <> + ; } diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index d2127af..f473ec7 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -10,12 +10,11 @@ import "../styles/Layout.scss"; export default function(ownProps: any) { const props = useSelector((state: RootState) => { return { - initialized: state.web3.networkID, + initialized: state.web3.networkID !== undefined, networkID: state.web3.networkID, error: state.web3.error, account: state.web3.account, type: state.web3.type, - sidebarOpen: state.layout.sidebarOpen, } }, shallowEqual); diff --git a/client/src/config.ts b/client/src/config.ts index c7b9024..df92ae6 100644 --- a/client/src/config.ts +++ b/client/src/config.ts @@ -11,6 +11,10 @@ export const config: Config = { export const recipientBucketsPath = "/recipients/:recipientAddress/buckets"; export const redeemablePath = "/buckets/:bucketAddress/redeemables/:recipientAddress"; +export const buildRecipientBucketsPath = (recipientAddress: string) => { + return `/recipients/${recipientAddress}/buckets`; +} + export const buildRedeemablePath = (bucketAddress: string, recipientAddress: string) => { return `/buckets/${bucketAddress}/redeemables/${recipientAddress}`; } diff --git a/client/src/reducers/layout.ts b/client/src/reducers/layout.ts index ca200a7..eea4abb 100644 --- a/client/src/reducers/layout.ts +++ b/client/src/reducers/layout.ts @@ -1,30 +1,21 @@ import { LayoutActions, - LAYOUT_TOGGLE_SIDEBAR, LAYOUT_TOGGLE_DEBUG, LAYOUT_FLIP_CARD, } from "../actions/layout"; export interface LayoutState { - sidebarOpen: boolean, cardFlipped: boolean, debugOpen: boolean, } const initialState: LayoutState = { - sidebarOpen: false, cardFlipped: false, debugOpen: false, } export const layoutReducer = (state: LayoutState = initialState, action: LayoutActions): LayoutState => { switch(action.type) { - case LAYOUT_TOGGLE_SIDEBAR: - return { - ...state, - sidebarOpen: action.open, - }; - case LAYOUT_TOGGLE_DEBUG: return { ...state, diff --git a/client/src/reducers/web3.ts b/client/src/reducers/web3.ts index 53f3e95..02613eb 100644 --- a/client/src/reducers/web3.ts +++ b/client/src/reducers/web3.ts @@ -10,6 +10,7 @@ import { export interface Web3State { initialized: boolean networkID: number | undefined + chainID: number | undefined error: string | undefined account: string | undefined type: Web3Type @@ -18,6 +19,7 @@ export interface Web3State { const initialState: Web3State = { initialized: false, networkID: undefined, + chainID: undefined, error: undefined, account: undefined, type: Web3Type.None, @@ -44,6 +46,7 @@ export const web3Reducer = (state: Web3State = initialState, action: Web3Actions return { ...state, networkID: action.networkID, + chainID: action.chainID, } } diff --git a/client/src/utils.ts b/client/src/utils.ts index db4d1c7..1471eaf 100644 --- a/client/src/utils.ts +++ b/client/src/utils.ts @@ -7,6 +7,8 @@ import { import { AbiItem } from "web3-utils"; import Bucket from './contracts/Bucket.json'; import { config } from "./config"; +import { recoverTypedSignature } from 'eth-sig-util'; +import { Web3Type } from "./actions/web3"; // keccak256("") export const KECCAK_EMPTY_STRING = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"; @@ -39,3 +41,95 @@ export const newBucketContract = (address: string) => { const bucket = new config.web3!.eth.Contract(bucketAbi, address); return bucket; } + +export interface SignRedeemResponse { + sig: string + signer: string +} + +export const signTypedDataWithKeycard = (data: any): Promise => { + return new Promise((resolve, reject) => { + (window as any).ethereum.request({ + method: "keycard_signTypedData", + params: JSON.stringify(data) + }).then((sig: any) => { + const signer = recoverTypedSignature({ + data, + sig + }); + resolve({ sig, signer }); + }).catch((err: string) => { + alert("err") + reject(err); + }) + }); +} + +export const signTypedDataWithWeb3 = (signer: string, data: any): Promise => { + return new Promise((resolve, reject) => { + (window as any).ethereum.sendAsync({ + method: "eth_signTypedData_v3", + params: [signer, JSON.stringify(data)], + from: signer, + }, (err: string, resp: any) => { + if (err) { + reject(err); + } else { + const sig = resp.result; + const signer = recoverTypedSignature({ + data, + sig + }); + + resolve({ sig, signer }); + } + }) + }); +} + +//FIXME: use a proper message for authentication instead of KeycardERC20Bucket +export const signTypedLogin = async (chainID: number, signer: string, web3Type: Web3Type): Promise => { + const message = { + blockNumber: 1, + blockHash: "0x0000000000000000000000000000000000000000", + code: "0x0000000000000000000000000000000000000000", + receiver: signer, + } + + const domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" } + ]; + + const redeem = [ + { name: "blockNumber", type: "uint256" }, + { name: "blockHash", type: "bytes32" }, + { name: "receiver", type: "address" }, + { name: "code", type: "bytes32" }, + ]; + + const domainData = { + name: "KeycardERC20Bucket", + version: "1", + chainId: chainID, + verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", + }; + + const data = { + types: { + EIP712Domain: domain, + Redeem: redeem, + }, + primaryType: "Redeem", + domain: domainData, + message: message + }; + + if (web3Type === Web3Type.Status) { + return signTypedDataWithKeycard(data); + } else { + return signTypedDataWithWeb3(signer, data); + } +} diff --git a/client/yarn.lock b/client/yarn.lock index db414a2..a171464 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1798,6 +1798,16 @@ "@types/react" "*" "@types/react-router" "*" +"@types/react-router-redux@^5.0.18": + version "5.0.18" + resolved "https://registry.yarnpkg.com/@types/react-router-redux/-/react-router-redux-5.0.18.tgz#5f28d5f7387fa71e33f602ccf9269e1609d47b8b" + integrity sha512-5SI69Virpmo+5HXWXKIzSt5hsnV7TTidL3Ddmbi+PH1CIdi40wthJwjFoqiE+gRQANur5WhjEtfyPorJ4zymHA== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + redux ">= 3.7.2" + "@types/react-router@*": version "5.1.8" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa" @@ -9860,6 +9870,11 @@ react-router-dom@^5.1.2: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router-redux@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" + integrity sha1-InQDWWtRUeGCN32rg1tdRfD4BU4= + react-router@5.2.0, react-router@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" @@ -10061,7 +10076,7 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@^4.0.0, redux@^4.0.5: +"redux@>= 3.7.2", redux@^4.0.0, redux@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==