more emitted events
This commit is contained in:
parent
35a2671ab0
commit
e289bc2033
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
src/config.ts
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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));
|
||||
})
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
<Typography variant="caption" color="inherit">
|
||||
{walletAddress}
|
||||
{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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export const statusPayAddress ="0x0000000000000000000000000000000000000000";
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,9 @@
|
|||
import Web3 from 'web3';
|
||||
|
||||
export interface Config {
|
||||
web3: Web3 | undefined
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
web3: undefined,
|
||||
};
|
|
@ -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")
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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}`;
|
||||
};
|
|
@ -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
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue