add homepage with authentication

This commit is contained in:
Andrea Franz 2020-09-29 10:25:39 +02:00
parent 8df34f8040
commit e67f436320
No known key found for this signature in database
GPG Key ID: 4F0D2F2D9DE7F29D
13 changed files with 221 additions and 109 deletions

View File

@ -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",

View File

@ -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)
});
}
}

View File

@ -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,

View File

@ -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<SignRedeemResponse> {
const chainId = await config.web3!.eth.net.getId();
async function signRedeem(chainID: number, web3Type: Web3Type, contractAddress: string, signer: string, message: RedeemMessage, domainName: string): Promise<SignRedeemResponse> {
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<SignRedeemResponse> => {
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<SignRedeemResponse> => {
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) => {

View File

@ -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));

View File

@ -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<any>(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<any>(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);
}
});
}

View File

@ -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 <>
<button onClick={ () => dispatch<any>(start()) }>START</button>
</>;
}

View File

@ -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);

View File

@ -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}`;
}

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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<SignRedeemResponse> => {
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<SignRedeemResponse> => {
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<SignRedeemResponse> => {
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);
}
}

View File

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