more emitted events
This commit is contained in:
parent
35a2671ab0
commit
e289bc2033
|
@ -11,7 +11,9 @@ import "./BlockConsumer.sol";
|
||||||
import "./EVMUtils.sol";
|
import "./EVMUtils.sol";
|
||||||
|
|
||||||
contract StatusPay is BlockConsumer {
|
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 {
|
struct Payment {
|
||||||
uint256 blockNumber;
|
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));
|
return token.balanceOf(address(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
function balanceOf(address _account) external view returns (uint256) {
|
function balanceOf(address _account) public view returns (uint256) {
|
||||||
Account storage account = accounts[_account];
|
(, 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) {
|
if (!account.exists) {
|
||||||
account = accounts[owners[_account]];
|
accountAddress = owners[_addressOrOwnerOrKeycard];
|
||||||
|
account = accounts[accountAddress];
|
||||||
|
|
||||||
if (!account.exists) {
|
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) {
|
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");
|
require(token.transferFrom(msg.sender, address(this), _amount), "transfer failed");
|
||||||
|
|
||||||
topped.balance += _amount;
|
topped.balance += _amount;
|
||||||
|
emit TopUp(_id, _amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withdraw(uint256 _amount) public {
|
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.exists, "account does not exist");
|
||||||
require(exiting.balance >= _amount, "not enough balance");
|
require(exiting.balance >= _amount, "not enough balance");
|
||||||
|
|
||||||
exiting.balance -= _amount;
|
exiting.balance -= _amount;
|
||||||
require(token.transfer(msg.sender, _amount), "transfer failed");
|
require(token.transfer(msg.sender, _amount), "transfer failed");
|
||||||
|
emit Withdraw(acc, _amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unlockAccount(Unlock memory _unlock, bytes memory _signature) public {
|
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 {
|
function requestPayment(Payment memory _payment, bytes memory _signature) public {
|
||||||
address signer = ECDSA.recover(EVMUtils.eip712Hash(DOMAIN_SEPARATOR, hashPayment(_payment)), _signature);
|
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
|
(address payeeAddress, Account storage payee) = _resolveAccount(_payment.to);
|
||||||
if (!payer.exists) {
|
require(payee.exists, "payee account does not exist");
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that _payment.amount is not greater than the maxTxValue for this currency
|
// check that _payment.amount is not greater than the maxTxValue for this currency
|
||||||
require(_payment.amount <= payer.maxTxAmount, "amount not allowed");
|
require(_payment.amount <= payer.maxTxAmount, "amount not allowed");
|
||||||
|
@ -245,7 +252,7 @@ contract StatusPay is BlockConsumer {
|
||||||
|
|
||||||
// set new baseline block for checks
|
// set new baseline block for checks
|
||||||
payer.lastUsedBlock = blockNumber;
|
payer.lastUsedBlock = blockNumber;
|
||||||
emit NewPayment(_payment.to, _payment.amount);
|
emit NewPayment(payerAddress, payeeAddress, _payment.amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashPayment(Payment memory _payment) internal pure returns (bytes32) {
|
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);
|
await requestPaymentTest(10);
|
||||||
assert.fail("requestPayment should have failed");
|
assert.fail("requestPayment should have failed");
|
||||||
} catch (err) {
|
} 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'));
|
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 buyerAcc = await statusPay.owners.call(owner);
|
||||||
let merchantAcc = await statusPay.owners.call(merchant);
|
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(buyerAcc)).balance.toNumber(), 90);
|
||||||
assert.equal((await statusPay.accounts.call(merchantAcc)).balance.toNumber(), 10);
|
assert.equal((await statusPay.accounts.call(merchantAcc)).balance.toNumber(), 10);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue