diff --git a/apps/simple-wallet/src/actions/wallet.ts b/apps/simple-wallet/src/actions/wallet.ts new file mode 100644 index 0000000..4edd58e --- /dev/null +++ b/apps/simple-wallet/src/actions/wallet.ts @@ -0,0 +1,81 @@ +import { Dispatch } from 'redux'; +import { RootState } from '../reducers'; +import Web3 from 'web3'; +import { abi as keycardWalletFactoryABI } from '../contracts/KeycardWalletFactory'; +import { abi as keycardWalletABI } from '../contracts/KeycardWallet'; +import { isEmptyAddress } from '../utils'; + +const walletFactoryAddress = "0xF42dC64c5F580FFC120d6bc281C1293E63d8132a"; +const testKeycardAddress = "0xfD51b65f6Dee2aDd1C867c05d5B8d189b9da7060"; + +export const WALLET_FACTORY_LOADING = "WALLET_FACTORY_LOADING"; +export interface WalletFactoryAction { + type: typeof WALLET_FACTORY_LOADING + keycardAddress: string +} + +export const WALLET_KEYCARD_NOT_FOUND = "WALLET_KEYCARD_NOT_FOUND"; +export interface WalletKeycardNotFoundAction { + type: typeof WALLET_KEYCARD_NOT_FOUND + keycardAddress: string +} + +export const WALLET_LOADING = "WALLET_LOADING"; +export interface WalletLoadingAction { + type: typeof WALLET_LOADING + address: string +} + +export const WALLET_BALANCE_LOADED = "WALLET_BALANCE_LOADED"; +export interface WalletBalanceLoadedAction { + type: typeof WALLET_BALANCE_LOADED + balance: string +} + +export type WalletActions = + WalletFactoryAction | + WalletKeycardNotFoundAction | + WalletLoadingAction; + +export const loadingWalletFactory = (keycardAddress: string): WalletFactoryAction => ({ + type: WALLET_FACTORY_LOADING, + keycardAddress, +}); + +export const keycardNotFound = (keycardAddress: string): WalletKeycardNotFoundAction => ({ + type: WALLET_KEYCARD_NOT_FOUND, + keycardAddress, +}); + +export const loadingWallet = (address: string): WalletLoadingAction => ({ + type: WALLET_LOADING, + address, +}); + +export const balanceLoaded = (balance: string): WalletBalanceLoadedAction => ({ + type: WALLET_BALANCE_LOADED, + balance, +}); + +export const loadWallet = (dispatch: Dispatch) => { + const keycardAddress = testKeycardAddress; + + dispatch(loadingWalletFactory(keycardAddress)); + const web3: Web3 = (window as any).web3; + const factory = new web3.eth.Contract(keycardWalletFactoryABI, walletFactoryAddress); + factory.methods.keycardsWallets(keycardAddress).call().then((walletAddress: string) => { + if (isEmptyAddress(walletAddress)) { + dispatch(keycardNotFound(keycardAddress)); + return; + } + + dispatch(loadingWallet(walletAddress)); + const wallet = new web3.eth.Contract(keycardWalletABI, walletAddress); + return web3.eth.getBalance(walletAddress); + }).then((balance: string) => { + dispatch(balanceLoaded(balance)); + }).catch((error: string) => { + //FIXME: manage error + console.log("error", error) + }) +} diff --git a/apps/simple-wallet/src/actions/web3.ts b/apps/simple-wallet/src/actions/web3.ts new file mode 100644 index 0000000..1cc6d71 --- /dev/null +++ b/apps/simple-wallet/src/actions/web3.ts @@ -0,0 +1,69 @@ +import Web3 from 'web3'; +import { + Dispatch, + AnyAction, +} from 'redux'; +import { RootState } from '../reducers'; +import { loadWallet } from './wallet'; +import { ThunkAction } from 'redux-thunk'; + + +export const WEB3_INITIALIZED = "WEB3_INITIALIZED"; +export interface Web3InitializedAction { + type: typeof WEB3_INITIALIZED +} + +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 type Web3Actions = + Web3InitializedAction | + Web3ErrorAction | + Web3NetworkIDLoadedAction; + +export const web3Initialized = (): Web3Actions => { + return { + type: WEB3_INITIALIZED, + }; +} + +export const web3NetworkIDLoaded = (id: number): Web3Actions => { + return { + type: WEB3_NETWORK_ID_LOADED, + networkID: id, + }; +} + +export const web3Error = (error: string): Web3Actions => { + return { + type: WEB3_ERROR, + error: error, + }; +} + +export const initializeWeb3 = () => { + //FIXME: move to config + const web3 = new Web3('https://ropsten.infura.io/v3/f315575765b14720b32382a61a89341a'); + (window as any).web3 = web3; + + + return (dispatch: Dispatch, getState: () => RootState) => { + dispatch(web3Initialized()); + web3.eth.net.getId().then((id) => { + dispatch(web3NetworkIDLoaded(id)) + loadWallet(dispatch); + }).catch((err) => { + dispatch(web3Error(err)) + }) + } +} + diff --git a/apps/simple-wallet/src/contracts/KeycardWallet.ts b/apps/simple-wallet/src/contracts/KeycardWallet.ts new file mode 100644 index 0000000..c0e7f77 --- /dev/null +++ b/apps/simple-wallet/src/contracts/KeycardWallet.ts @@ -0,0 +1,246 @@ +import { AbiItem } from 'web3-utils'; + +export const abi: AbiItem[] = [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "bytes3" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "hash", + "type": "bytes32" + }, + { + "name": "sig", + "type": "bytes" + } + ], + "name": "recover", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "pure", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_keycard", + "type": "address" + } + ], + "name": "setKeycard", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "keycard", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_maxTxValue", + "type": "uint256" + } + ], + "name": "setSettings", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "nonce", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_hashToSign", + "type": "bytes32" + }, + { + "name": "_signature", + "type": "bytes" + }, + { + "name": "_nonce", + "type": "uint256" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "requestPayment", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "settings", + "outputs": [ + { + "name": "maxTxValue", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "pendingWithdrawals", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "name": "_name", + "type": "bytes3" + }, + { + "name": "_keycard", + "type": "address" + }, + { + "name": "_maxTxValue", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "nonce", + "type": "uint256" + }, + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "NewPaymentRequest", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "NewWithdrawal", + "type": "event" + } +] diff --git a/apps/simple-wallet/src/contracts/KeycardWalletFactory.ts b/apps/simple-wallet/src/contracts/KeycardWalletFactory.ts new file mode 100644 index 0000000..f24526f --- /dev/null +++ b/apps/simple-wallet/src/contracts/KeycardWalletFactory.ts @@ -0,0 +1,104 @@ +import { AbiItem } from 'web3-utils'; + +export const abi: AbiItem[] = [ + { + "constant": false, + "inputs": [ + { + "name": "name", + "type": "bytes3" + }, + { + "name": "keycard", + "type": "address" + }, + { + "name": "maxTxValue", + "type": "uint256" + } + ], + "name": "create", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "name": "ownerWalletsCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "keycardsWallets", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "uint256" + } + ], + "name": "ownersWallets", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "wallet", + "type": "address" + }, + { + "indexed": false, + "name": "name", + "type": "bytes3" + } + ], + "name": "NewWallet", + "type": "event", + } +] diff --git a/apps/simple-wallet/src/index.tsx b/apps/simple-wallet/src/index.tsx index 2efdb64..06c71bd 100644 --- a/apps/simple-wallet/src/index.tsx +++ b/apps/simple-wallet/src/index.tsx @@ -8,6 +8,7 @@ import { Middleware, MiddlewareAPI, Dispatch, + AnyAction, } from 'redux' import { initializeWeb3 } from './actions/web3'; import { createRootReducer } from './reducers' diff --git a/apps/simple-wallet/src/reducers/web3.ts b/apps/simple-wallet/src/reducers/web3.ts new file mode 100644 index 0000000..0eabce4 --- /dev/null +++ b/apps/simple-wallet/src/reducers/web3.ts @@ -0,0 +1,45 @@ +import { + Web3Actions, + WEB3_INITIALIZED, + WEB3_ERROR, + WEB3_NETWORK_ID_LOADED, +} from '../actions/web3'; + +export interface Web3State { + initialized: boolean + networkID: number | undefined + error: string | undefined +} + +const initialState = { + initialized: false, + networkID: undefined, + error: undefined, +}; + +export function web3Reducer(state: Web3State = initialState, action: Web3Actions): Web3State { + switch (action.type) { + case WEB3_INITIALIZED: { + return { + ...state, + initialized: true, + } + } + + case WEB3_ERROR: { + return { + ...state, + error: action.error, + } + } + + case WEB3_NETWORK_ID_LOADED: { + return { + ...state, + networkID: action.networkID, + } + } + } + + return state; +} diff --git a/apps/simple-wallet/tsconfig.json b/apps/simple-wallet/tsconfig.json new file mode 100644 index 0000000..f2850b7 --- /dev/null +++ b/apps/simple-wallet/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" + ] +}