more emitted events

This commit is contained in:
Michele Balistreri 2020-10-28 14:40:00 +01:00
parent 35a2671ab0
commit e289bc2033
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
32 changed files with 14142 additions and 28 deletions

View File

@ -11,7 +11,9 @@ import "./BlockConsumer.sol";
import "./EVMUtils.sol";
contract StatusPay is BlockConsumer {
event NewPayment(address to, uint256 amount);
event NewPayment(address from, address to, uint256 amount);
event TopUp(address account, uint256 amount);
event Withdraw(address account, uint256 amount);
struct Payment {
uint256 blockNumber;
@ -70,22 +72,35 @@ contract StatusPay is BlockConsumer {
));
}
function totalSupply() external view returns (uint256) {
function totalSupply() public view returns (uint256) {
return token.balanceOf(address(this));
}
function balanceOf(address _account) external view returns (uint256) {
Account storage account = accounts[_account];
function balanceOf(address _account) public view returns (uint256) {
(, Account memory account) = _resolveAccount(_account);
return account.balance;
}
function resolveAccount(address _addressOrOwnerOrKeycard) public view returns (address) {
(address addr, ) = _resolveAccount(_addressOrOwnerOrKeycard);
return addr;
}
function _resolveAccount(address _addressOrOwnerOrKeycard) internal view returns (address, Account storage) {
address accountAddress = _addressOrOwnerOrKeycard;
Account storage account = accounts[accountAddress];
if (!account.exists) {
account = accounts[owners[_account]];
accountAddress = owners[_addressOrOwnerOrKeycard];
account = accounts[accountAddress];
if (!account.exists) {
account = accounts[keycards[_account]];
accountAddress = keycards[_addressOrOwnerOrKeycard];
account = accounts[accountAddress];
}
}
return account.balance;
return (accountAddress, account);
}
function name() public view returns (string memory) {
@ -180,15 +195,18 @@ contract StatusPay is BlockConsumer {
require(token.transferFrom(msg.sender, address(this), _amount), "transfer failed");
topped.balance += _amount;
emit TopUp(_id, _amount);
}
function withdraw(uint256 _amount) public {
Account storage exiting = accounts[owners[msg.sender]];
address acc = owners[msg.sender];
Account storage exiting = accounts[acc];
require(exiting.exists, "account does not exist");
require(exiting.balance >= _amount, "not enough balance");
exiting.balance -= _amount;
require(token.transfer(msg.sender, _amount), "transfer failed");
emit Withdraw(acc, _amount);
}
function unlockAccount(Unlock memory _unlock, bytes memory _signature) public {
@ -211,22 +229,11 @@ contract StatusPay is BlockConsumer {
function requestPayment(Payment memory _payment, bytes memory _signature) public {
address signer = ECDSA.recover(EVMUtils.eip712Hash(DOMAIN_SEPARATOR, hashPayment(_payment)), _signature);
Account storage payer = accounts[keycards[signer]];
(address payerAddress, Account storage payer) = _resolveAccount(signer);
require(payer.exists, "payer account not found");
// allow direct payment without Keycard from owner
if (!payer.exists) {
payer = accounts[owners[signer]];
require(payer.exists, "no account for this Keycard");
}
// check that the payee exists
Account storage payee = accounts[_payment.to];
// allow payment through owner address
if (!payee.exists) {
payee = accounts[owners[_payment.to]];
require(payee.exists, "payee account does not exist");
}
(address payeeAddress, Account storage payee) = _resolveAccount(_payment.to);
require(payee.exists, "payee account does not exist");
// check that _payment.amount is not greater than the maxTxValue for this currency
require(_payment.amount <= payer.maxTxAmount, "amount not allowed");
@ -245,7 +252,7 @@ contract StatusPay is BlockConsumer {
// set new baseline block for checks
payer.lastUsedBlock = blockNumber;
emit NewPayment(_payment.to, _payment.amount);
emit NewPayment(payerAddress, payeeAddress, _payment.amount);
}
function hashPayment(Payment memory _payment) internal pure returns (bytes32) {

View File

@ -0,0 +1 @@
src/config.ts

View File

@ -0,0 +1,51 @@
{
"name": "simple-wallet",
"version": "0.0.1",
"description": "",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"keywords": [],
"author": "",
"license": "MIT",
"eslintConfig": {
"extends": "react-app"
},
"dependencies": {
"@material-ui/core": "^4.7.0",
"@material-ui/icons": "^4.5.1",
"@material-ui/styles": "^4.6.0",
"qrcode-generator-ts": "^0.0.4",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.1",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"typeface-roboto": "^0.0.75",
"web3": "^1.2.4"
},
"devDependencies": {
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
"react-scripts": "3.2.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^6.1.2",
"typescript": "^3.6.3"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,10 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Keycard Simple Wallet</title>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,53 @@
import { Dispatch } from 'redux';
import { config } from '../global';
import { RootState } from '../reducers';
export const BLOCK_LOADING = "BLOCK_LOADING";
export interface BlockLoadingAction {
type: typeof BLOCK_LOADING
number: number
}
export const BLOCK_LOADED = "BLOCK_LOADED";
export interface BlockLoadedAction {
type: typeof BLOCK_LOADED
number: number
timestamp: number
}
export type BlocksActions =
BlockLoadingAction |
BlockLoadedAction;
export const blockLoaded = (number: number, timestamp: number): BlockLoadedAction => ({
type: BLOCK_LOADED,
number,
timestamp,
});
export const loadingBlock = (number: number): BlockLoadingAction => ({
type: BLOCK_LOADING,
number,
});
export const loadBlock = (number: number) => {
return (dispatch: Dispatch, getState: () => RootState) => {
if (getState().blocks[number] !== undefined) {
// block already loaded
return;
}
dispatch(loadingBlock(number))
config.web3!.eth.getBlock(number).then(b => {
let timestamp;
if (typeof b.timestamp === "string") {
timestamp = parseInt(b.timestamp);
} else {
timestamp = b.timestamp;
}
dispatch(blockLoaded(b.number, timestamp));
})
};
}

View File

@ -0,0 +1,161 @@
import { Dispatch } from 'redux';
import { RootState } from '../reducers';
import { Contract } from 'web3-eth-contract';
import Web3 from 'web3';
import { TransactionReceipt } from 'web3-core';
import { loadWalletBalance } from './wallet';
import { loadBlock } from './blocks';
import { addPadding } from "../utils";
export const TXS_LOADING = "TXS_LOADING";
export interface TxsLoadingAction {
type: typeof TXS_LOADING
}
export const TXS_LOADED = "TXS_LOADED";
export interface TxsLoadedAction {
type: typeof TXS_LOADED
}
export const TXS_TRANSACTION_DISCOVERED = "TXS_TRANSACTION_DISCOVERED";
export interface TxsTransactionDiscoveredAction {
type: typeof TXS_TRANSACTION_DISCOVERED
event: string
pending: boolean
id: string
blockNumber: number
transactionHash: string
from: string | undefined
to: string | undefined
value: string
}
export const TXS_TRANSACTION_CONFIRMED = "TXS_TRANSACTION_CONFIRMED";
export interface TxsTransactionConfirmedAction {
type: typeof TXS_TRANSACTION_CONFIRMED
transactionHash: string
}
export type TxsActions =
TxsLoadingAction |
TxsLoadedAction |
TxsTransactionDiscoveredAction |
TxsTransactionConfirmedAction;
export const transactionDiscovered = (event: string, id: string, blockNumber: number, transactionHash: string, pending: boolean, from: string, to: string | undefined, value: string): TxsTransactionDiscoveredAction => ({
type: TXS_TRANSACTION_DISCOVERED,
event,
id,
blockNumber,
transactionHash,
pending,
from,
to,
value,
});
export const loadingTransactions = (): TxsLoadingAction => ({
type: TXS_LOADING,
});
export const transactionsLoaded = (): TxsLoadedAction => ({
type: TXS_LOADED,
});
export const transactionConfirmed = (transactionHash: string): TxsTransactionConfirmedAction => ({
type: TXS_TRANSACTION_CONFIRMED,
transactionHash,
});
export const loadTransactions = (web3: Web3, statusPay: Contract) => {
return (dispatch: Dispatch, getState: () => RootState) => {
const state = getState();
const walletAddress = state.wallet.walletAddress;
if (walletAddress === undefined) {
return;
}
web3.eth.getBlockNumber().then((blockNumber: number) => {
dispatch<any>(getPastTransactions(web3, statusPay, walletAddress, blockNumber, "to"))
dispatch<any>(getPastTransactions(web3, statusPay, walletAddress, blockNumber, "from"))
dispatch<any>(subscribeToTransactions(web3, statusPay, walletAddress, blockNumber, "to"))
dispatch<any>(subscribeToTransactions(web3, statusPay, walletAddress, blockNumber, "from"))
});
};
}
type ToOrFrom = "to" | "from";
const getPastTransactions = (web3: Web3, statusPay: Contract, walletAddress: string, fromBlock: number, toOrFrom: ToOrFrom) => {
return (dispatch: Dispatch, getState: () => RootState) => {
const paddedWalletAddress = addPadding(64, walletAddress);
const eventType = toOrFrom === "to" ? "TopUp" : "NewPayment";
const topics: (null | string)[] = [null, null, null];
switch(toOrFrom) {
case "from":
topics[1] = paddedWalletAddress;
break;
case "to":
topics[2] = paddedWalletAddress;
break;
}
const options = {
fromBlock: 0,
toBlock: "latest",
topics: topics,
};
dispatch(loadingTransactions());
statusPay.getPastEvents("Transfer", options).then((events: any) => {
events.forEach((event: any) => {
const values = event.returnValues;
dispatch<any>(loadBlock(event.blockNumber));
dispatch(transactionDiscovered(eventType, event.id, event.blockNumber, event.transactionHash, false, values.from, walletAddress, values.value));
});
dispatch(transactionsLoaded());
});
};
};
const subscribeToTransactions = (web3: Web3, erc20: Contract, walletAddress: string, fromBlock: number, toOrFrom: ToOrFrom) => {
return (dispatch: Dispatch, getState: () => RootState) => {
const options = {
fromBlock: fromBlock,
filter: {
[toOrFrom]: walletAddress,
}
};
const eventType = toOrFrom === "to" ? "TopUp" : "NewPayment";
erc20.events.Transfer(options).on('data', (event: any) => {
const values = event.returnValues;
const pending = event.blockHash === null;
dispatch(transactionDiscovered(eventType, event.id, event.blockNumber, event.transactionHash, pending, values.from, walletAddress, values.value));
dispatch<any>(loadWalletBalance(web3, undefined));
if (pending) {
watchPendingTransaction(web3, dispatch, walletAddress, event.transactionHash);
}
});
};
};
export const watchPendingTransaction = (web3: Web3, dispatch: Dispatch, walletAddress: string | undefined, transactionHash: string) => {
web3.eth.getTransactionReceipt(transactionHash).then((tx: TransactionReceipt) => {
if (tx.status) {
dispatch(transactionConfirmed(transactionHash));
if (walletAddress !== undefined) {
dispatch<any>(loadWalletBalance(web3, undefined));
}
return;
}
window.setTimeout(() => watchPendingTransaction(web3, dispatch, walletAddress, transactionHash), 5000)
}).catch((error: string) => {
//FIXME: handle error
console.log("error", error)
});
}

View File

@ -0,0 +1,288 @@
import { Dispatch } from 'redux';
import { RootState } from '../reducers';
import Web3 from 'web3';
import { abi as StatusPayABI } from '../../../../build/contracts/StatusPay.json';
import { isEmptyAddress } from '../utils';
import { loadTransactions } from './transactions';
import { Contract } from 'web3-eth-contract';
import { statusPayAddress } from "../config";
export const ERR_WALLET_NOT_FOUND = "ERR_WALLET_NOT_FOUND";
export interface ErrWalletNotFound {
type: typeof ERR_WALLET_NOT_FOUND
keycardAddress: string
}
export const ERR_ERC20_NOT_SET = "ERR_ERC20_NOT_SET";
export interface ErrERC20NotSet {
type: typeof ERR_ERC20_NOT_SET
}
export const ERR_GETTING_ERC20_SYMBOL = "ERR_GETTING_ERC20_SYMBOL";
export interface ErrGettingERC20Symbol {
type: typeof ERR_GETTING_ERC20_SYMBOL
erc20Address: string
error: string
}
export const ERR_LOADING_BALANCE = "ERR_LOADING_BALANCE";
export interface ErrLoadingBalance {
type: typeof ERR_LOADING_BALANCE
erc20Address: string
walletAddress: string
error: string
}
export type WalletErrors =
ErrWalletNotFound |
ErrERC20NotSet |
ErrGettingERC20Symbol |
ErrLoadingBalance;
export const WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED = "WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED";
export interface WalletKeycardAddressNotSpecifiedAction {
type: typeof WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED
}
export const WALLET_INVALID_KEYCARD_ADDRESS = "WALLET_INVALID_KEYCARD_ADDRESS";
export interface WalletInvalidKeycardAddressAction {
type: typeof WALLET_INVALID_KEYCARD_ADDRESS
}
export const WALLET_FACTORY_LOADING_ERC20_ADDRESS = "WALLET_FACTORY_LOADING_ERC20_ADDRESS";
export interface WalletFactoryLoadingERC20AddressAction {
type: typeof WALLET_FACTORY_LOADING_ERC20_ADDRESS
}
export const WALLET_FACTORY_ERC20_ADDRESS_LOADED = "WALLET_FACTORY_ERC20_ADDRESS_LOADED";
export interface WalletFactoryERC20AddressLoadedAction {
type: typeof WALLET_FACTORY_ERC20_ADDRESS_LOADED
address: string
}
export const WALLET_FACTORY_LOADING_ERC20_SYMBOL = "WALLET_FACTORY_LOADING_ERC20_SYMBOL";
export interface WalletFactoryLoadingERC20SymbolAction {
type: typeof WALLET_FACTORY_LOADING_ERC20_SYMBOL
}
export const WALLET_FACTORY_ERC20_SYMBOL_LOADED = "WALLET_FACTORY_ERC20_SYMBOL_LOADED";
export interface WalletFactoryERC20SymbolLoadedAction {
type: typeof WALLET_FACTORY_ERC20_SYMBOL_LOADED
symbol: string
}
export const WALLET_FACTORY_LOADING_WALLET_ADDRESS = "WALLET_FACTORY_LOADING_WALLET_ADDRESS";
export interface WalletFactoryLoadingWalletAddressAction {
type: typeof WALLET_FACTORY_LOADING_WALLET_ADDRESS
keycardAddress: string
}
export const WALLET_FACTORY_KEYCARD_NOT_FOUND = "WALLET_FACTORY_KEYCARD_NOT_FOUND";
export interface WalletFactoryKeycardNotFoundAction {
type: typeof WALLET_FACTORY_KEYCARD_NOT_FOUND
keycardAddress: string
}
export const WALLET_FACTORY_WALLET_ADDRESS_LOADED = "WALLET_FACTORY_WALLET_ADDRESS_LOADED";
export interface WalletFactoryWalletAddressLoadedAction {
type: typeof WALLET_FACTORY_WALLET_ADDRESS_LOADED
keycardAddress: string
walletAddress: string
}
export const WALLET_LOADING_BALANCE = "WALLET_LOADING_BALANCE";
export interface WalletLoadingBalanceAction {
type: typeof WALLET_LOADING_BALANCE
address: string
}
export const WALLET_BALANCE_LOADED = "WALLET_BALANCE_LOADED";
export interface WalletBalanceLoadedAction {
type: typeof WALLET_BALANCE_LOADED
balance: string
availableBalance: string
}
export const WALLET_TOGGLE_QRCODE = "WALLET_TOGGLE_QRCODE";
export interface WalletToggleQRCodeAction {
type: typeof WALLET_TOGGLE_QRCODE
open: boolean
}
export type WalletActions =
WalletKeycardAddressNotSpecifiedAction |
WalletInvalidKeycardAddressAction |
WalletFactoryLoadingERC20AddressAction |
WalletFactoryERC20AddressLoadedAction |
WalletFactoryLoadingERC20SymbolAction |
WalletFactoryERC20SymbolLoadedAction |
WalletFactoryLoadingWalletAddressAction |
WalletFactoryWalletAddressLoadedAction |
WalletFactoryKeycardNotFoundAction |
WalletLoadingBalanceAction |
WalletBalanceLoadedAction |
WalletToggleQRCodeAction;
export const keycardAddressNotSpecified = (): WalletKeycardAddressNotSpecifiedAction => ({
type: WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED,
});
export const invalidKeycardAddress = (): WalletInvalidKeycardAddressAction => ({
type: WALLET_INVALID_KEYCARD_ADDRESS,
});
export const loadingERC20Address = (): WalletFactoryLoadingERC20AddressAction => ({
type: WALLET_FACTORY_LOADING_ERC20_ADDRESS,
});
export const erc20AddressLoaded = (address: string): WalletFactoryERC20AddressLoadedAction => ({
type: WALLET_FACTORY_ERC20_ADDRESS_LOADED,
address,
});
export const loadingERC20Symbol = (): WalletFactoryLoadingERC20SymbolAction => ({
type: WALLET_FACTORY_LOADING_ERC20_SYMBOL,
});
export const erc20SymbolLoaded = (symbol: string): WalletFactoryERC20SymbolLoadedAction => ({
type: WALLET_FACTORY_ERC20_SYMBOL_LOADED,
symbol,
});
export const loadingWalletAddress = (keycardAddress: string): WalletFactoryLoadingWalletAddressAction => ({
type: WALLET_FACTORY_LOADING_WALLET_ADDRESS,
keycardAddress,
});
export const walletAddressLoaded = (keycardAddress: string, walletAddress: string): WalletFactoryWalletAddressLoadedAction => ({
type: WALLET_FACTORY_WALLET_ADDRESS_LOADED,
keycardAddress,
walletAddress,
});
export const keycardNotFound = (keycardAddress: string): WalletFactoryKeycardNotFoundAction => ({
type: WALLET_FACTORY_KEYCARD_NOT_FOUND,
keycardAddress,
});
export const loadingWalletBalance = (address: string): WalletLoadingBalanceAction => ({
type: WALLET_LOADING_BALANCE,
address,
});
export const balanceLoaded = (balance: string, availableBalance: string): WalletBalanceLoadedAction => ({
type: WALLET_BALANCE_LOADED,
balance,
availableBalance,
});
export const showWalletQRCode = (): WalletToggleQRCodeAction => ({
type: WALLET_TOGGLE_QRCODE,
open: true,
});
export const hideWalletQRCode = (): WalletToggleQRCodeAction => ({
type: WALLET_TOGGLE_QRCODE,
open: false,
});
export const loadWallet = async (web3: Web3, dispatch: Dispatch, getState: () => RootState) => {
const params = new URLSearchParams(window.location.search);
const keycardAddress = params.get("address");
if (keycardAddress === null) {
dispatch(keycardAddressNotSpecified());
return
}
if (!web3.utils.isAddress(keycardAddress)) {
dispatch(invalidKeycardAddress());
return;
}
const statusPay = new web3.eth.Contract(StatusPayABI as any, statusPayAddress);
dispatch<any>(loadWalletAddress(web3, statusPay, keycardAddress))
.then(() => dispatch<any>(loadERC20(web3, statusPay)))
.then(() => dispatch<any>(loadERC20Symbol(web3, statusPay)))
.then(() => dispatch<any>(loadWalletBalance(web3, statusPay)))
.then(() => dispatch<any>(loadTransactions(web3, statusPay)))
.catch((err: string) => {
console.error("global catch", err)
return;
})
}
const loadWalletAddress = (web3: Web3, statusPay: Contract, keycardAddress: string) => {
return async (dispatch: Dispatch) => {
dispatch(loadingWalletAddress(keycardAddress));
return statusPay.methods.keycards(keycardAddress).call().then((address: string) => {
if (isEmptyAddress(address)) {
dispatch(keycardNotFound(address));
throw({
type: ERR_WALLET_NOT_FOUND,
keycardAddress: keycardAddress,
});
}
dispatch(walletAddressLoaded(keycardAddress, address));
}).catch((err: any) => {
console.error("err", err)
throw(err)
});
}
}
const loadERC20 = (web3: Web3, statusPay: Contract) => {
return async (dispatch: Dispatch) => {
return statusPay.methods.token().call().then((address: string) => {
if (isEmptyAddress(address)) {
throw({ type: ERR_ERC20_NOT_SET });
}
dispatch(erc20AddressLoaded(address));
}).catch((err: any) => {
console.error("err", err)
throw(err);
});
}
}
const loadERC20Symbol = (web3: Web3, statusPay: Contract) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(loadingERC20Symbol());
return statusPay.methods.symbol().call().then((symbol: string) => {
dispatch(erc20SymbolLoaded(symbol));
}).catch((err: string) => {
console.error("err", err)
throw({
type: ERR_GETTING_ERC20_SYMBOL,
erc20Address: getState().wallet.erc20Address!,
error: err,
});
});
}
}
export const loadWalletBalance = (web3: Web3, _statusPay: Contract | undefined) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
const state = getState();
const address = state.wallet.walletAddress!;
const statusPay = _statusPay !== undefined ? _statusPay : new web3.eth.Contract(StatusPayABI as any, statusPayAddress);
dispatch(loadingWalletBalance(address));
return statusPay.methods.balanceOf(address).call().then((balance: string) => {
dispatch(balanceLoaded(balance, balance));
}).catch((err: string) => {
console.error("err", err)
throw({
type: ERR_LOADING_BALANCE,
erc20Address: state.wallet.erc20Address!,
walletAddress: address,
error: err,
});
});
}
}

View File

@ -0,0 +1,120 @@
import Web3 from 'web3';
import { config } from '../global';
import {
Dispatch,
} from 'redux';
import { RootState } from '../reducers';
import { loadWallet } from './wallet';
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 type Web3Actions =
Web3InitializedAction |
Web3ErrorAction |
Web3NetworkIDLoadedAction;
export const web3Initialized = (t: Web3Type): Web3Actions => ({
type: WEB3_INITIALIZED,
web3Type: t,
})
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 = () => {
const w = window as any;
if (w.ethereum) {
config.web3 = new Web3(w.ethereum);
return (dispatch: Dispatch, getState: () => RootState) => {
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))
loadWallet(config.web3!, dispatch, getState);
});
})
.catch((err: string) => {
//FIXME: handle error
console.log("error", err)
});
}
} else if (config.web3) {
return (dispatch: Dispatch, getState: () => RootState) => {
const t: Web3Type = w.ethereum.isStatus ? Web3Type.Status : Web3Type.Generic;
dispatch(web3Initialized(t));
config.web3!.eth.net.getId().then((id: number) => {
dispatch(web3NetworkIDLoaded(id))
loadWallet(config.web3!, dispatch, getState);
})
.catch((err: string) => {
//FIXME: handle error
console.log("error", err)
});
}
} 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'));
// alert(`remote`)
config.web3 = new Web3(new Web3.providers.WebsocketProvider('wss://ropsten.infura.io/ws/v3/f315575765b14720b32382a61a89341a'));
return (dispatch: Dispatch, getState: () => RootState) => {
dispatch(web3Initialized(Web3Type.Remote));
config.web3!.eth.net.getId().then((id: number) => {
dispatch(web3NetworkIDLoaded(id))
loadWallet(config.web3!, dispatch, getState);
})
.catch((err: string) => {
//FIXME: handle error
console.log("error", err)
});
}
}
}

View File

@ -0,0 +1,106 @@
import React from 'react';
import TransactionsList from '../containers/TransactionsList';
import TopPanel from '../containers/TopPanel';
import ReceiveDialog from '../containers/ReceiveDialog';
import { makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import CssBaseline from '@material-ui/core/CssBaseline';
import CircularProgress from '@material-ui/core/CircularProgress';
import { compressedAddress } from '../utils';
import { Props } from '../containers/App';
const useStyles = makeStyles(theme => ({
container: {
paddingTop: 64,
[theme.breakpoints.only('xs')]: {
// paddingTop: 48,
paddingTop: 56,
},
},
loading: {
textAlign: "center",
marginTop: 100,
display: "inline-block",
width: "100%",
},
main: {
position: 'relative'
},
error: {
position: 'absolute',
width: '100%',
top: '0',
left: '0',
textAlign: 'center',
background: 'red',
color: 'white',
padding: "8px 0",
fontWeight: "bold",
},
progress: {
color: "rgb(14, 28, 54)",
},
}));
const App = (props: Props) => {
const classes = useStyles();
const loading = <div className={classes.loading}>
<CircularProgress disableShrink className={classes.progress}></CircularProgress>
</div>;
let body = <></>;
if (props.web3Error !== undefined) {
return <div className={classes.error}>
{props.web3Error}
</div>;
}
//FIXME: check if loading
if (props.walletError === undefined && props.loading) {
body = loading;
} else if (!props.loading) {
body = <>
<TopPanel />
<TransactionsList />
</>;
}
const networkText = props.networkID ? `(Net ID: ${props.networkID})` : "";
const walletAddress = props.walletAddress ? compressedAddress(props.walletAddress) : "";
return (
<div className={classes.container}>
<CssBaseline />
<AppBar style={{ backgroundColor: "#0e1c36" }}>
<Toolbar>
<Typography variant="h6" color="inherit">
Keycard Wallet &nbsp;
<Typography variant="caption" color="inherit">
{walletAddress} &nbsp;
{networkText}
</Typography>
</Typography>
</Toolbar>
</AppBar>
<div className={classes.main}>
{body}
{props.walletError !== undefined && <div className={classes.error}>
{props.walletError}
</div>}
<ReceiveDialog />
</div>
</div>
);
};
export default App;

View File

@ -0,0 +1,115 @@
// import React from 'react';
// import Button from '@material-ui/core/Button';
// import Typography from '@material-ui/core/Typography';
// import Dialog from '@material-ui/core/Dialog';
// import DialogActions from '@material-ui/core/DialogActions';
// import DialogContent from '@material-ui/core/DialogContent';
// import DialogContentText from '@material-ui/core/DialogContentText';
// import DialogTitle from '@material-ui/core/DialogTitle';
// import Slide from '@material-ui/core/Slide';
// import TextField from '@material-ui/core/TextField';
// import CircularProgress from '@material-ui/core/CircularProgress';
// import Divider from '@material-ui/core/Divider';
// import InputAdornment from '@material-ui/core/InputAdornment';
// const icons = ["💳", "👛", "💸", "😺"]
// const iconStyles = {
// fontSize: "2em",
// marginRight: 16,
// padding: 4,
// cursor: "pointer"
// }
// const selectedIconStyles = {
// ...iconStyles,
// border: "4px solid #999"
// }
// const errorStyles = {
// color: "red",
// }
// function Transition(props) {
// return <Slide direction="up" {...props} />;
// }
// const NewWalletDialog = ({open, creating, selected, keycardAddress, onIconClick, onCancelClick, onCreateClick, onKeycardChange, error, onTapButtonClick, onMaxTxValueChange, maxTxValue}) => (
// <Dialog
// fullScreen
// TransitionComponent={Transition}
// open={open}
// aria-labelledby="alert-dialog-title"
// aria-describedby="alert-dialog-description"
// >
// <DialogTitle id="alert-dialog-title">New Wallet</DialogTitle>
// <DialogContent>
// <Typography variant="body2" gutterBottom>
// Choose an icon to identify your wallet
// </Typography>
// <div style={{marginBottom: 16}}>
// {icons.map((icon) =>
// <a key={icon} onClick={() => onIconClick(icon) }
// style={selected == icon ? selectedIconStyles : iconStyles}>{icon}</a>
// )}
// </div>
// <TextField
// margin="dense"
// label="Maximum transaction value"
// type="text"
// value={maxTxValue}
// style={{marginBottom: 16}}
// fullWidth
// onChange={(event) => onMaxTxValueChange(event.currentTarget.value)}
// InputProps={{
// startAdornment: <InputAdornment position="start" style={{paddingBottom: 7}}>Ξ</InputAdornment>,
// }}
// />
// <TextField
// margin="dense"
// label="Keycard address"
// type="text"
// value={keycardAddress}
// style={{marginBottom: 16}}
// fullWidth
// onChange={(event) => onKeycardChange(event.currentTarget.value)}
// />
// <div style={{margin: 16, textAlign: "center"}}>
// <Typography variant="body2" gutterBottom>
// or
// </Typography>
// </div>
// <Button onClick={() => onTapButtonClick("hello world")} size="large" color="primary" variant="contained" fullWidth>
// CONNECT TAPPING YOUR KEYCARD
// </Button>
// {creating && <div style={{marginTop: 50, textAlign: "center"}}>
// <CircularProgress />
// </div>}
// </DialogContent>
// <DialogActions>
// {!creating &&
// <Button onClick={onCancelClick} color="primary">
// Cancel
// </Button>}
// <Button onClick={onCreateClick} color="primary" autoFocus disabled={(selected && !creating) ? false : true}>
// {!creating ? "Create" : "Creating..."}
// </Button>
// </DialogActions>
// {error &&
// <DialogActions>
// <div style={errorStyles}>{error}</div>
// </DialogActions>}
// </Dialog>
// );
// export default NewWalletDialog;

View File

@ -0,0 +1,81 @@
import React, { useEffect } from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Slide from '@material-ui/core/Slide';
import { Props } from '../containers/ReceiveDialog';
import { TransitionProps } from '@material-ui/core/transitions';
import { makeStyles } from '@material-ui/core/styles';
import { QRCode, QR8BitByte } from 'qrcode-generator-ts';
const useStyles = makeStyles(theme => ({
qrdialog: {
width: "100%",
},
container: {
textAlign: "center",
},
qrcode: {
width: "100%",
margin: "0 auto",
}
}));
const Transition = React.forwardRef<unknown, TransitionProps>((props, ref) => {
return <Slide direction="up" ref={ref} {...props} />;
});
const ReceiveDialog = (props: Props) => {
const classes = useStyles();
const image = React.useRef<HTMLImageElement>(null);
useEffect(() => {
if (props.tokenAddress === undefined ||
props.tokenSymbol === undefined ||
props.address === undefined ||
props.networkID === undefined ||
image.current === null) {
return;
}
const uri = `ethereum:${props.tokenAddress}@${props.networkID}/transfer?address=${props.address}`;
const qr = new QRCode();
qr.setTypeNumber(6);
qr.addData(new QR8BitByte(uri));
qr.make();
image.current.src = qr.toDataURL(4);
}, [props.tokenAddress, props.tokenSymbol, props.address, props.networkID, image]);
return <Dialog
fullWidth={true}
maxWidth="xs"
className={classes.qrdialog}
open={props.open}
TransitionComponent={Transition}
keepMounted
onClose={props.handleClose}
aria-labelledby="alert-dialog-slide-title"
aria-describedby="alert-dialog-slide-description">
<DialogTitle id="alert-dialog-slide-title">Send only {props.tokenSymbol} tokens</DialogTitle>
<DialogContent className={classes.container}>
<div>
<img alt={props.address} className={classes.qrcode} ref={image} />
</div>
<div>
<pre>
{props.address}
</pre>
</div>
</DialogContent>
<DialogActions>
<Button onClick={props.handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>;
}
export default ReceiveDialog;

View File

@ -0,0 +1,68 @@
import React from 'react';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import { Props } from '../containers/TopPanel';
import { config } from '../global';
const useStyles = makeStyles(theme => ({
container: {
position: 'relative',
height: 200,
// backgroundImage: 'linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%)',
backgroundImage: 'linear-gradient(120deg, #fa709a 0%, #fee140 100%)',
border: "none",
boxShadow: "rgba(0, 0, 0, 0.8) 0 0 8px 0",
color: '#FFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
actions: {
textAlign: "center",
paddingTop: 20,
},
button: {
color: "#fff",
borderColor: "#fff",
"&:hover": {
borderColor: "#fff",
}
},
}));
const roundEther = (wei: string | undefined) => {
const fullTotal = wei ? config.web3!.utils.fromWei(wei) : "0";
const parts = fullTotal.split(".");
let roundedBalance = parts[0];
const decimals = (parts[1] || "").slice(0, 4)
if (decimals.length > 0) {
roundedBalance = `${roundedBalance}.${decimals}`;
}
return roundedBalance;
}
const TopPanel = (props: Props) => {
const classes = useStyles();
const roundedBalance = roundEther(props.balance);
return <div className={classes.container}>
<div>
<Typography variant="h2" color="inherit">
{roundedBalance} {props.tokenSymbol}
</Typography>
<div className={classes.actions}>
<Button
className={classes.button}
variant="outlined"
color="primary"
onClick={props.handleReceive}>Receive</Button>
</div>
</div>
</div>
}
export default TopPanel;

View File

@ -0,0 +1,52 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TransactionsListItem from '../containers/TransactionsListItem';
import { Props } from '../containers/TransactionsList';
import List from '@material-ui/core/List';
import CircularProgress from '@material-ui/core/CircularProgress';
const useStyles = makeStyles(theme => ({
addButton: {
position: 'fixed',
zIndex: 1,
bottom: 30,
right: 30,
},
loading: {
textAlign: "center",
marginTop: 32,
},
empty: {
padding: 10,
textAlign: "center",
},
wrongNetwork: {
padding: 10,
textAlign: "center",
background: "red",
color: "#fff",
fontWeight: "bold",
},
progress: {
color: "rgb(14, 28, 54)",
},
}));
const WalletsList = (props: Props) => {
const classes = useStyles();
return (<>
{props.loading && <div className={classes.loading}>
<CircularProgress className={classes.progress} disableShrink></CircularProgress>
</div>}
{<List>
{props.transactions.map((tx) => (
<TransactionsListItem key={tx.id} id={tx.id} />
))}
</List>}
</>
)
};
export default WalletsList;

View File

@ -0,0 +1,154 @@
import React from 'react'
import { makeStyles } from '@material-ui/core/styles';
import TransactionInIcon from '@material-ui/icons/CallReceived';
import TransactionOutIcon from '@material-ui/icons/CallMade';
import TransactionUnknownIcon from '@material-ui/icons/HelpOutline';
import { withStyles } from '@material-ui/core/styles';
import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar';
import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider';
import CircularProgress from '@material-ui/core/CircularProgress';
import Fade from '@material-ui/core/Fade';
import { Props } from '../containers/TransactionsListItem';
import {
compressedAddress,
} from '../utils';
const StyledListItemText = withStyles({
secondary: {
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
},
})(ListItemText);
const useStyles = makeStyles(theme => ({
block: {
display: "block"
},
timestamp: {
display: "block",
fontStyle: "italic",
color: "rgba(0, 0, 0, 0.54)",
fontSize: 12,
},
avatar: {
backgroundColor: '#fff',
boxShadow: "rgba(180, 180, 180, 0.6) 0 0 5px 1px",
backgroundImage: 'linear-gradient(120deg, rgba(114, 255, 132, 0.5) 0%, rgba(255, 225, 255, 0.5) 100%)',
},
avatarIn: {
backgroundColor: '#fff',
boxShadow: "rgba(180, 180, 180, 0.6) 0 0 5px 1px",
extend: "avatar",
backgroundImage: 'linear-gradient(120deg, rgba(114, 255, 132, 0.5) 0%, rgba(255, 225, 255, 0.5) 100%)',
},
avatarOut: {
backgroundColor: '#fff',
boxShadow: "rgba(180, 180, 180, 0.6) 0 0 5px 1px",
extend: "avatar",
backgroundImage: 'linear-gradient(120deg, rgba(255, 114, 114, 0.5) 0%, rgba(255, 225, 255, 0.5) 100%)',
},
avatarLoading: {
position: "absolute",
top: 0,
left: 0,
},
icon: {
color: "#000",
},
iconIn: {
color: "green",
},
iconOut: {
color: "red",
},
}));
const icon = (event: string, className: any) => {
switch(event) {
case "TopUp":
return <TransactionInIcon className={className} />
case "NewPayment":
case "Withdraw":
return <TransactionOutIcon className={className} />
default:
return <TransactionUnknownIcon />
}
};
export function timestampToString(timestamp: number | undefined) {
if (timestamp === undefined) {
return "";
}
return new Date(timestamp * 1000).toLocaleDateString('default', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
}
const TransactionsListItem = (props: Props) => {
const classes = useStyles();
const tx = props.transactions[props.id];
if (tx === undefined) {
return null;
}
const fromAddress = tx.from ? compressedAddress(tx.from, 8) : "";
const toAddress = tx.to ? compressedAddress(tx.to, 8) : "";
let date = "";
const block = props.blocks[tx.blockNumber];
if (block !== undefined) {
date = timestampToString(block.timestamp);
}
const primary = <span>
<span className={classes.block}>{tx.valueInETH} Ξ</span>
<span className={classes.timestamp}>{date}</span>
</span>;
const secondary = <span>
<span className={classes.block}>{tx.blockNumber} from: {fromAddress}</span>
<span className={classes.block}>to: {toAddress}</span>
</span>;
const [avatarClass, iconClass] = (event => {
switch(event) {
case "TopUp":
return [classes.avatarIn, classes.iconIn];
case "NewPayment":
case "Withdraw":
return [classes.avatarOut, classes.iconOut];
default:
return [classes.avatar, classes.icon];
}
})(tx.event)
return (
<>
<ListItem button>
<ListItemAvatar>
<Fade in={true} timeout={800}>
<Avatar className={avatarClass}>
{(tx.pending === true || tx.pending === undefined)
&& <CircularProgress color="secondary" className={classes.avatarLoading}/>}
{icon(tx.event, iconClass)}
</Avatar>
</Fade>
</ListItemAvatar>
<StyledListItemText primary={primary} secondary={secondary} />
</ListItem>
<Divider />
</>
);
};
export default TransactionsListItem;

View File

@ -0,0 +1 @@
export const statusPayAddress ="0x0000000000000000000000000000000000000000";

View File

@ -0,0 +1,37 @@
import { connect } from 'react-redux';
import App from '../components/App';
import { RootState } from '../reducers';
import { Dispatch } from 'redux';
export interface StateProps {
loading: boolean
web3Error: string | undefined
walletAddress: string | undefined
networkID: number | undefined
walletError: string | undefined
}
export interface DispatchProps {
}
export type Props = StateProps & DispatchProps;
const mapStateToProps = (state: RootState): StateProps => {
const ready = state.web3.initialized && state.wallet.ready;
return {
loading: !ready,
web3Error: state.web3.error,
networkID: state.web3.networkID,
walletAddress: state.wallet.walletAddress,
walletError: state.wallet.error,
}
};
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -0,0 +1,33 @@
// import { connect } from 'react-redux';
// import NewWalletDialog from '../components/NewWalletDialog';
// import {
// newWalletSelectIcon,
// newWalletCancel,
// createWallet,
// newWalletFormKeycardAddressChanged,
// newWalletFormMaxTxValueChanged,
// signMessagePinless,
// } from '../actions';
// const mapStateToProps = state => ({
// open: state.newWalletForm.open,
// selected: state.newWalletForm.icon,
// creating: state.newWalletForm.creating,
// error: (state.newWalletForm.error || "").toString(),
// keycardAddress: state.newWalletForm.keycardAddress || "",
// maxTxValue: state.newWalletForm.maxTxValue,
// });
// const mapDispatchToProps = dispatch => ({
// onIconClick: (icon) => dispatch(newWalletSelectIcon(icon)),
// onCancelClick: () => dispatch(newWalletCancel()),
// onCreateClick: () => dispatch(createWallet()),
// onKeycardChange: (address) => dispatch(newWalletFormKeycardAddressChanged(address)),
// onMaxTxValueChange: (value) => dispatch(newWalletFormMaxTxValueChanged(value)),
// onTapButtonClick: (message) => dispatch(signMessagePinless(message)),
// });
// export default connect(
// mapStateToProps,
// mapDispatchToProps
// )(NewWalletDialog);

View File

@ -0,0 +1,42 @@
import { connect } from 'react-redux';
import { RootState } from '../reducers';
import { MouseEvent } from 'react';
import { Dispatch } from 'redux';
import ReceiveDialog from '../components/ReceiveDialog';
import { hideWalletQRCode } from '../actions/wallet';
export interface StateProps {
open: boolean
tokenSymbol: string | undefined
tokenAddress: string | undefined
address: string | undefined
networkID: number | undefined
}
export interface DispatchProps {
handleClose: (e: MouseEvent) => any
}
export type Props = StateProps & DispatchProps;
const mapStateToProps = (state: RootState): StateProps => {
return {
open: state.wallet.showWalletQRCode,
tokenSymbol: state.wallet.erc20Symbol,
tokenAddress: state.wallet.erc20Address,
address: state.wallet.walletAddress,
networkID: state.web3.networkID,
}
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleClose: (e: MouseEvent) => {
e.preventDefault();
dispatch(hideWalletQRCode());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReceiveDialog);

View File

@ -0,0 +1,38 @@
import { connect } from 'react-redux';
import { RootState } from '../reducers';
import { MouseEvent } from 'react';
import { Dispatch } from 'redux';
import TopPanel from '../components/TopPanel';
import { showWalletQRCode } from '../actions/wallet';
export interface StateProps {
tokenSymbol: string | undefined
balance: string | undefined
availableBalance: string | undefined
}
export interface DispatchProps {
handleReceive: (e: MouseEvent) => any
}
export type Props = StateProps & DispatchProps;
const mapStateToProps = (state: RootState): StateProps => {
return {
balance: state.wallet.balance,
tokenSymbol: state.wallet.erc20Symbol,
availableBalance: state.wallet.availableBalance,
}
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleReceive: (e: MouseEvent) => {
e.preventDefault();
dispatch(showWalletQRCode());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TopPanel);

View File

@ -0,0 +1,46 @@
import { connect } from 'react-redux';
import { RootState } from '../reducers';
import { Dispatch } from 'redux';
import TransactionsList from '../components/TransactionsList';
import { TransactionState } from '../reducers/transactions';
export interface StateProps {
loading: boolean
transactions: TransactionState[]
}
export interface DispatchProps {
}
export type Props = StateProps & DispatchProps;
const newProps = (): Props => {
return {
loading: false,
transactions: [],
}
}
const mapStateToProps = (state: RootState): StateProps => {
const props = newProps();
const transactions: TransactionState[] = [];
if (!props.loading) {
for (const txHash in state.transactions.transactions) {
transactions.unshift(state.transactions.transactions[txHash]);
}
}
return {
...props,
loading: state.transactions.loadingRequests > 0,
transactions: transactions,
}
};
const mapDispatchToProps = (dispatch: Dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TransactionsList);

View File

@ -0,0 +1,42 @@
import { connect } from 'react-redux';
import { RootState } from '../reducers';
import { Dispatch } from 'redux';
import TransactionsListItem from '../components/TransactionsListItem';
import { TransactionState } from '../reducers/transactions';
import { BlocksState } from '../reducers/blocks';
interface Transactions {
[txHash: string]: TransactionState
}
export interface StateProps {
id: string
key: string,
transactions: Transactions
blocks: BlocksState
}
export interface OwnProps {
id: string,
key: string,
}
export interface DispatchProps {}
export type Props = StateProps & DispatchProps;
const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
return {
id: ownProps.id,
key: ownProps.key,
transactions: state.transactions.transactions,
blocks: state.blocks,
}
};
const mapDispatchToProps = (dispatch: Dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TransactionsListItem);

View File

@ -0,0 +1,9 @@
import Web3 from 'web3';
export interface Config {
web3: Web3 | undefined
}
export const config: Config = {
web3: undefined,
};

View File

@ -0,0 +1,61 @@
import React 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 { initializeWeb3 } from './actions/web3';
import { createRootReducer } from './reducers'
import 'typeface-roboto';
import App from './containers/App';
// FIXME: remove, use a built-in one
interface Action {
(...args: any[]): any
}
const loggerMiddleware: Middleware = ({ getState }: MiddlewareAPI) => (
next: Dispatch
) => action => {
console.log('will dispatch', action)
// Call the next dispatch method in the middleware chain.
const returnValue = next(action)
console.log('state after dispatch', getState())
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue
}
let middlewares: Middleware[] = [
thunkMiddleware,
];
if (process.env.NODE_ENV !== 'production') {
middlewares = [
...middlewares,
loggerMiddleware,
];
}
const store = createStore(
createRootReducer(),
applyMiddleware(...middlewares),
);
store.dispatch<any>(initializeWeb3());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

View File

@ -0,0 +1,47 @@
import {
BLOCK_LOADING,
BLOCK_LOADED,
BlocksActions,
} from '../actions/blocks';
export interface BlockState {
number: number
timestamp: number | undefined
}
export interface BlocksState {
[blockNumber: number]: BlockState
};
const initialState = {};
export const blocksReducer = (state: BlocksState = initialState, action: BlocksActions): BlocksState => {
switch (action.type) {
case BLOCK_LOADING: {
return {
...state,
[action.number]: {
number: action.number,
timestamp: undefined,
}
}
}
case BLOCK_LOADED: {
const blockState = state[action.number];
if (blockState === undefined) {
return state;
}
return {
...state,
[action.number]: {
...blockState,
timestamp: action.timestamp,
}
}
}
}
return state;
}

View File

@ -0,0 +1,31 @@
import { combineReducers } from 'redux';
import {
Web3State,
web3Reducer,
} from './web3';
import {
WalletState,
walletReducer,
} from './wallet';
import {
TransactionsState,
transactionsReducer,
} from './transactions';
import {
BlocksState,
blocksReducer,
} from './blocks';
export interface RootState {
web3: Web3State,
wallet: WalletState,
transactions: TransactionsState,
blocks: BlocksState,
}
export const createRootReducer = () => combineReducers({
web3: web3Reducer,
wallet: walletReducer,
transactions: transactionsReducer,
blocks: blocksReducer,
});

View File

@ -0,0 +1,115 @@
import { config } from '../global';
import {
TXS_LOADING,
TXS_LOADED,
TXS_TRANSACTION_DISCOVERED,
TXS_TRANSACTION_CONFIRMED,
TxsActions,
} from '../actions/transactions';
export interface TransactionState {
id: string
blockNumber: number
event: string
transactionHash: string
pending: boolean | undefined
from: string | undefined
to: string | undefined
value: string
valueInETH: string
}
export interface TransactionsState {
loadingRequests: number
transactions: {
[txHash: string]: TransactionState
}
};
const newTransactionState = (): TransactionState => ({
id: "",
blockNumber: 0,
event: "",
transactionHash: "",
pending: undefined,
from: undefined,
to: undefined,
value: "",
valueInETH: "",
});
const initialState: TransactionsState = {
loadingRequests: 0,
transactions: {},
};
export const transactionsReducer = (state: TransactionsState = initialState, action: TxsActions): TransactionsState => {
switch (action.type) {
case TXS_LOADING: {
return {
...state,
loadingRequests: state.loadingRequests + 1,
}
}
case TXS_LOADED: {
return {
...state,
loadingRequests: state.loadingRequests - 1,
}
}
case TXS_TRANSACTION_DISCOVERED: {
const txState: TransactionState = state.transactions[action.transactionHash] || newTransactionState();
// if tx was is new (pending === undefined)
// OR tx was pending
// then set the current state
// otherwise if tx was confirmed, leave it confirmed in case a watcher is getting
// an old event
if (txState.pending === undefined || txState.pending) {
txState.pending = action.pending;
}
const valueInETH = config.web3!.utils.fromWei(action.value);
return {
...state,
transactions: {
...state.transactions,
[action.id]: {
...txState,
id: action.id,
blockNumber: action.blockNumber,
event: action.event,
transactionHash: action.transactionHash,
pending: action.pending,
from: action.from,
to: action.to,
value: action.value,
valueInETH: valueInETH,
}
}
}
}
case TXS_TRANSACTION_CONFIRMED: {
const txState: TransactionState = state.transactions[action.transactionHash];
if (txState === undefined) {
return state;
}
return {
...state,
transactions: {
...state.transactions,
[action.transactionHash]: {
...txState,
pending: false,
}
}
}
}
}
return state;
}

View File

@ -0,0 +1,124 @@
import {
WalletActions,
WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED,
WALLET_INVALID_KEYCARD_ADDRESS,
WALLET_FACTORY_ERC20_ADDRESS_LOADED,
WALLET_FACTORY_ERC20_SYMBOL_LOADED,
WALLET_FACTORY_LOADING_WALLET_ADDRESS,
WALLET_FACTORY_KEYCARD_NOT_FOUND,
WALLET_FACTORY_WALLET_ADDRESS_LOADED,
WALLET_LOADING_BALANCE,
WALLET_BALANCE_LOADED,
WALLET_TOGGLE_QRCODE,
} from '../actions/wallet';
export interface WalletState {
ready: boolean
loading: boolean
erc20Address: string | undefined
erc20Symbol: string | undefined
keycardAddress: string | undefined
walletAddress: string | undefined
walletFound: boolean
balance: string | undefined
availableBalance: string | undefined
error: string | undefined
showWalletQRCode: boolean
}
const initialState = {
ready: false,
loading: false,
erc20Address: undefined,
erc20Symbol: undefined,
keycardAddress: undefined,
walletAddress: undefined,
walletFound: false,
balance: undefined,
availableBalance: undefined,
error: undefined,
showWalletQRCode: false,
};
export const walletReducer = (state: WalletState = initialState, action: WalletActions): WalletState => {
switch (action.type) {
case WALLET_TOGGLE_QRCODE: {
return {
...state,
showWalletQRCode: action.open,
}
}
case WALLET_KEYCARD_ADDRESS_NOT_SPECIFIED: {
return {
...state,
error: "Keycard address not specified. Tap your keycard on your phone.",
}
}
case WALLET_INVALID_KEYCARD_ADDRESS: {
return {
...state,
error: "invalid keycard address",
}
}
case WALLET_FACTORY_LOADING_WALLET_ADDRESS: {
return {
...state,
loading: true,
keycardAddress: action.keycardAddress,
}
}
case WALLET_FACTORY_KEYCARD_NOT_FOUND: {
return {
...state,
loading: false,
error: "not wallet found for the selected Keycard address",
}
}
case WALLET_FACTORY_ERC20_ADDRESS_LOADED: {
return {
...state,
erc20Address: action.address,
}
}
case WALLET_FACTORY_ERC20_SYMBOL_LOADED: {
return {
...state,
erc20Symbol: action.symbol,
}
}
case WALLET_FACTORY_WALLET_ADDRESS_LOADED: {
return {
...state,
loading: false,
walletFound: true,
walletAddress: action.walletAddress,
}
}
case WALLET_LOADING_BALANCE: {
return {
...state,
loading: true,
}
}
case WALLET_BALANCE_LOADED: {
return {
...state,
ready: true,
loading: false,
balance: action.balance,
availableBalance: action.availableBalance,
}
}
}
return state
}

View File

@ -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: Web3State = {
initialized: false,
networkID: undefined,
error: undefined,
};
export const 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;
}

View File

@ -0,0 +1,20 @@
export const emptyAddress = "0x0000000000000000000000000000000000000000";
export const compressedAddress = (a: string, padding: number = 4) => {
return `${a.slice(0, padding + 2)}...${a.slice(a.length - padding)}`;
}
export const isEmptyAddress = (a: string) =>
a === emptyAddress;
export const addPadding = (n: number, hex: string) => {
if (hex.startsWith("0x")) {
hex = hex.slice(2);
}
for (let i = hex.length; i < n; i++) {
hex = `0${hex}`;
}
return `0x${hex}`;
};

View File

@ -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"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,7 @@ contract('StatusPay', (accounts) => {
await requestPaymentTest(10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "no account for this Keycard");
assert.equal(err.reason, "payer account not found");
}
});
@ -181,12 +181,13 @@ contract('StatusPay', (accounts) => {
const event = receipt.logs.find(element => element.event.match('NewPayment'));
assert.equal(event.args.to, merchant);
assert.equal(event.args.amount, 10);
let buyerAcc = await statusPay.owners.call(owner);
let merchantAcc = await statusPay.owners.call(merchant);
assert.equal(event.args.from, buyerAcc);
assert.equal(event.args.to, merchantAcc);
assert.equal(event.args.amount, 10);
assert.equal((await statusPay.accounts.call(buyerAcc)).balance.toNumber(), 90);
assert.equal((await statusPay.accounts.call(merchantAcc)).balance.toNumber(), 10);
});