import dapp

This commit is contained in:
Michele Balistreri 2020-10-02 11:37:56 +02:00
parent 7cedde3b84
commit b1f2c2f0b7
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
15 changed files with 1702 additions and 707 deletions

View File

@ -171,21 +171,18 @@ contract StatusPay is BlockConsumer {
// allow direct payment without Keycard from owner
if (!payer.exists) {
payer = accounts[owners[signer]];
require(payer.exists, "no account for this Keycard");
}
// check that a keycard is associated to this account
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");
}
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");

View File

View File

View File

@ -0,0 +1,20 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Keycard POS</title>
<!-- Sets initial viewport load and disables zooming -->
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<style type="text/css" media="screen">
hmtl, body { margin: 0; padding: 0;}
</style>
</head>
<body>
<div id="root"></div>
<script>
window.statusWeb3 = window.web3;
</script>
<script src="/js/app.js"></script>
</body>
</html>

View File

View File

@ -0,0 +1,328 @@
const StatusPay = artifacts.require('StatusPay');
import { recoverTypedSignature } from 'eth-sig-util';
const addressZero = "0x0000000000000000000000000000000000000000";
const tokenAddress = "0xc55cF4B03948D7EBc8b9E8BAD92643703811d162";
export const NEW_WALLET = 'NEW_WALLET';
export const newWallet = () => ({
type: NEW_WALLET,
});
export const ETHEREUM_LOAD_ERROR = 'ETHEREUM_LOAD_ERROR';
export const ethereumLoadError = (err) => ({
type: ETHEREUM_LOAD_ERROR,
err: err
});
export const ETHEREUM_LOADED = 'ETHEREUM_LOADED';
export const ethereumLoaded = () => ({
type: ETHEREUM_LOADED
});
export const LOADING_OWNER = 'LOADING_OWNER';
export const loadingOwner = () => ({
type: LOADING_OWNER
});
export const OWNER_LOADED = 'OWNER_LOADED';
export const ownerLoaded = (owner) => ({
type: OWNER_LOADED,
owner
});
export const WEB3_ERROR = 'WEB3_ERROR';
export const web3Error = (error) => ({
type: WEB3_ERROR,
error
});
export const LOADING_WALLETS = 'LOADING_WALLETS';
export const loadingWallets = () => ({
type: LOADING_WALLETS
});
export const WALLETS_LOADED = 'WALLETS_LOADED';
export const walletsLoaded = (wallets) => ({
type: WALLETS_LOADED,
wallets
});
export const COUNTING_WALLETS = 'COUNTING_WALLETS';
export const countingWallets = () => ({
type: COUNTING_WALLETS
});
export const WALLETS_COUNTED = 'WALLETS_COUNTED';
export const walletsCounted = (count) => ({
type: WALLETS_COUNTED,
count: parseInt(count)
});
export const LOADING_WALLET = 'LOADING_WALLET';
export const loadingWallet = () => ({
type: LOADING_WALLET,
});
export const WALLET_LOADED = 'WALLET_LOADED';
export const walletLoaded = (balance, maxTxValue) => ({
type: WALLET_LOADED,
balance,
maxTxValue,
});
export const NETWORK_ID_LOADED = "NETWORK_ID_LOADED";
export const networkIDLoaded = (id) => ({
type: NETWORK_ID_LOADED,
id
});
export const loadNetworkID = () => {
return (dispatch) => {
web3.eth.net.getId()
.then((id) => dispatch(networkIDLoaded(parseInt(id))))
.catch((err) => {
dispatch(web3Error(err))
})
}
}
function signPaymentRequest(getState, message, cb) {
const state = getState();
let domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
];
let payment = [
{ name: "blockNumber", type: "uint256" },
{ name: "blockHash", type: "bytes32" },
{ name: "currency", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "to", type: "address" }
];
let domainData = {
name: "StatusPay",
version: "1",
chainId: state.networkID,
verifyingContract: StatusPay.address
};
const data = {
types: {
EIP712Domain: domain,
Payment: payment
},
primaryType: "Payment",
domain: domainData,
message: message
};
const dataString = JSON.stringify(data);
const signer = state.owner;
if (window.ethereum && window.ethereum.isStatus) {
window.ethereum.send("keycard_signTypedData", [dataString])
.then(resp => cb(undefined, resp, data))
.catch(err => cb(err, undefined, data));
} else {
web3.currentProvider.sendAsync({
method: "eth_signTypedData_v3",
params: [signer, dataString],
from: signer
}, (err, resp) => cb(err, resp, data));
}
}
export const enableEthereum = () => {
if (window.ethereum) {
window.web3 = new Web3(ethereum);
return (dispatch) => {
ethereum.enable()
.then(() => {
window.setTimeout(() => {
dispatch(ethereumLoaded());
dispatch(loadNetworkID());
dispatch(loadOwner());
}, 200)
})
.catch((err) => {
dispatch(ethereumLoadError(err));
})
}
} else if (window.web3) {
return (dispatch) => {
dispatch(ethereumLoaded());
window.setTimeout(() => {
dispatch(loadNetworkID());
dispatch(loadOwner())
}, 200)
}
} else {
return ethereumLoadError("no ethereum browser");
}
};
export const loadOwner = () => {
return (dispatch) => {
dispatch(loadingOwner())
return web3.eth.getAccounts()
.then((accounts) => {
const owner = accounts[0];
dispatch(ownerLoaded(owner))
dispatch(loadOwnerBalance(owner))
})
.catch((err) => {
dispatch(web3Error(err))
});
}
};
export const OWNER_BALANCE_LOADED = "OWNER_BALANCE_LOADED";
export const ownerBalanceLoaded = (balance) => ({
type: OWNER_BALANCE_LOADED,
balance
});
export const loadOwnerBalance = (owner) => {
return (dispatch) => {
web3.eth.getBalance(owner)
.then((balance) => dispatch(ownerBalanceLoaded(balance)))
.catch((err) => dispatch(ethereumLoadError(err)))
}
}
export const REQUESTING_PAYMENT = "REQUESTING_PAYMENT";
export const requestingPayment = () => ({
type: REQUESTING_PAYMENT,
});
export const PAYMENT_REQUESTED = "PAYMENT_REQUESTED";
export const paymentRequested = () => ({
type: PAYMENT_REQUESTED,
});
export const sendPaymentRequest = (walletContract, message, sig) => {
return async (dispatch) => {
try {
dispatch(requestingPayment())
const requestPayment = await walletContract.methods.requestPayment(message, sig);
const gas = await requestPayment.estimateGas();
const receipt = await requestPayment.send({
gas: gas
});
dispatch(paymentRequested())
} catch(err) {
alert(err)
console.error("ERROR: ", err)
}
}
}
export const loadWallet = (walletAddress, message, sig) => {
return async (dispatch) => {
dispatch(loadingWallet())
const jsonInterface = KeycardWallet.options.jsonInterface;
const walletContract = new EmbarkJS.Blockchain.Contract({
abi: jsonInterface,
address: walletAddress,
});
walletContract.address = walletAddress;
const erc20Contract = new EmbarkJS.Blockchain.Contract({
abi: IERC20.options.jsonInterface,
address: tokenAddress,
});
erc20Contract.address = tokenAddress;
const balance = await erc20Contract.methods.balanceOf(walletAddress).call();
const maxTxValue = await walletContract.methods.tokenMaxTxAmount(tokenAddress).call();
dispatch(walletLoaded(balance, maxTxValue))
dispatch(sendPaymentRequest(walletContract, message, sig))
};
}
export const KEYCARD_DISCOVERED = "KEYCARD_DISCOVERED";
export const keycardDiscovered = (address) => ({
type: KEYCARD_DISCOVERED,
address,
});
export const FINDING_WALLET = "FINDING_WALLET";
export const findingWallet = () => ({
type: FINDING_WALLET,
});
export const WALLET_FOUND = "WALLET_FOUND";
export const walletFound = (address) => ({
type: WALLET_FOUND,
address,
});
export const PAYMENT_AMOUNT_VALUE_CHANGE = "PAYMENT_AMOUNT_VALUE_CHANGE";
export const paymentAmountValueChange = (value) => ({
type: PAYMENT_AMOUNT_VALUE_CHANGE,
value
});
export const findWallet = (keycardAddress, message, sig) => {
return async (dispatch) => {
dispatch(findingWallet());
KeycardWalletFactory.methods.keycardsWallets(keycardAddress).call()
.then((address) => {
//FIXME: if 0x00, display error
if (address === addressZero) {
alert("wallet not found")
return
}
dispatch(walletFound(address))
dispatch(loadWallet(address, message, sig))
})
.catch((err) => {
dispatch(web3Error(err))
});
}
}
export const requestPayment = () => {
return async (dispatch, getState) => {
const state = getState();
let block = await web3.eth.getBlock("latest");
const message = {
blockNumber: block.number,
blockHash: block.hash,
currency: tokenAddress,
to: state.owner,
amount: state.txAmount,
}
try {
signPaymentRequest(getState, message, function(err, response, data) {
if (err) {
dispatch(web3Error(err))
} else {
const sig = response.result;
const address = recoverTypedSignature({
data: data,
sig: sig,
})
dispatch(keycardDiscovered(address));
dispatch(findWallet(address, message, sig));
}
})
} catch(err) {
dispatch(web3Error(err))
}
};
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import Pos from "../containers/Pos"
import KeycardWalletFactory from 'Embark/contracts/KeycardWalletFactory';
import { withStyles } 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 CircularProgress from '@material-ui/core/CircularProgress';
import { compressedAddress } from '../utils';
const styles = theme => ({
loading: {
textAlign: "center",
padding: 50
}
});
const App = (props) => {
const loading = <div className={props.classes.loading}>
<CircularProgress />
</div>;
let body = loading;
if (!props.loadingWeb3 && !props.loadingOwner) {
body = <Pos />
}
const networkText = props.networkID ? `(Net ID: ${props.networkID})` : "";
return (
<div>
<AppBar style={{ backgroundColor: "#0e1c36", position: "relative" }}>
<Toolbar>
<Typography variant="h6" color="inherit">
Keycard POS
<Typography variant="caption" color="inherit">
{compressedAddress(KeycardWalletFactory.address)} {networkText}
</Typography>
</Typography>
</Toolbar>
</AppBar>
<div>
{body}
</div>
</div>
);
};
export default withStyles(styles)(App);

View File

@ -0,0 +1,80 @@
import React from 'react';
import Paper from '@material-ui/core/Paper';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import CircularProgress from '@material-ui/core/CircularProgress';
import Fab from '@material-ui/core/Fab';
import CheckIcon from '@material-ui/icons/Check';
import InputAdornment from '@material-ui/core/InputAdornment';
export const compressedAddress = (a, padding) => {
padding = padding || 4;
return `${a.slice(0, padding + 2)}...${a.slice(a.length - padding)}`
}
const formattedBalance = (balance) => {
if (balance) {
return web3.utils.fromWei(new web3.utils.BN(balance));
}
return "";
}
const Pos = ({requestingPayment, paymentRequested, onTapRequest, customerKeycardAddress, findingWallet, customerWalletAddress, loadingWallet, customerWallet, onAmountChange, txAmount}) => {
return (
<div style={{paddingTop: 32}}>
{!requestingPayment && !paymentRequested &&
<div style={{marginBottom: 32, textAlign: "center"}}>
<TextField margin="dense" label="Transaction amount" type="text" style={{marginBottom: 16}} fullWidth
onChange={(event) => onAmountChange(event.currentTarget.value)}
value = {txAmount}
InputProps={{startAdornment: <InputAdornment position="start" style={{paddingBottom: 7}}>Ξ</InputAdornment>}} />
<Button onClick={() => onTapRequest()} size="large" color="primary" variant="contained">
REQUEST PAYMENT
</Button>
</div>
}
{requestingPayment &&
<div>
<div style={{textAlign: "center"}}>
<CircularProgress />
</div>
{customerKeycardAddress && <p>
Customer Keycard Address: {compressedAddress(customerKeycardAddress)}
</p>}
{findingWallet && <p>
finding wallet...
</p>}
{customerWalletAddress && <p>
Wallet Address: {compressedAddress(customerWalletAddress)}
</p>}
{loadingWallet && <p>
loading wallet...
</p>}
{customerWallet && <p>
<strong>Wallet</strong><br />
Balance: {formattedBalance(customerWallet.balance)} <br />
Max Tx Value: {formattedBalance(customerWallet.maxTxValue)} <br />
</p>}
</div>
}
{paymentRequested &&
<div style={{textAlign: "center"}}>
<Fab color="primary">
<CheckIcon />
</Fab>
</div>
}
</div>
);
};
export default Pos;

View File

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import App from '../components/App';
const mapStateToProps = state => ({
loadingWeb3: state.loadingWeb3,
loadingOwner: state.loadingOwner,
networkID: state.networkID,
});
const mapDispatchToProps = dispatch => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

View File

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import Pos from '../components/Pos';
import {
requestPayment,
paymentAmountValueChange
} from "../actions";
const mapStateToProps = state => ({
customerKeycardAddress: state.customer.keycardAddress,
customerWalletAddress: state.customer.walletAddress,
customerWallet: state.customer.wallet,
findingWallet: state.findingWallet,
loadingWallet: state.loadingWallet,
requestingPayment: state.requestingPayment,
paymentRequested: state.paymentRequested,
txAmount: state.txAmount
});
const mapDispatchToProps = dispatch => ({
onTapRequest: () => dispatch(requestPayment()),
onAmountChange: (value) => dispatch(paymentAmountValueChange(value)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Pos);

View File

@ -0,0 +1,29 @@
import React from 'react';
import ReactDOM from 'react-dom';
import thunkMiddleware from 'redux-thunk';
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import { enableEthereum } from './actions';
import rootReducer from './reducers'
import 'typeface-roboto';
import { install } from '@material-ui/styles';
install();
import App from './containers/App';
const store = createStore(rootReducer,
applyMiddleware(
thunkMiddleware
)
);
store.dispatch(enableEthereum());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

View File

@ -0,0 +1,134 @@
import {
ETHEREUM_LOAD_ERROR,
WEB3_ERROR,
ETHEREUM_LOADED,
NETWORK_ID_LOADED,
LOADING_OWNER,
OWNER_LOADED,
OWNER_BALANCE_LOADED,
LOADING_WALLETS,
WALLETS_LOADED,
COUNTING_WALLETS,
WALLETS_COUNTED,
LOADING_WALLET,
WALLET_LOADED,
KEYCARD_DISCOVERED,
FINDING_WALLET,
WALLET_FOUND,
REQUESTING_PAYMENT,
PAYMENT_REQUESTED,
PAYMENT_AMOUNT_VALUE_CHANGE
} from "../actions";
const customerInitialState = {
keycardAddress: null,
walletAddress: null,
wallet: null,
}
const initialState = {
loadingWeb3: true,
loadingWeb3Error: null,
loadingOwner: false,
owner: null,
ownerBalance: null,
customer: customerInitialState,
findingWallet: false,
loadingWallet: false,
requestingPayment: false,
paymentRequested: false,
txAmount: 0
};
export default function(state, action) {
console.log("action", action)
console.log("state", state)
if (typeof state === 'undefined') {
return initialState;
}
switch (action.type) {
case ETHEREUM_LOAD_ERROR:
console.error(action.error)
return Object.assign({}, state, {
loadingWeb3: false,
loadingWeb3Error: action.err
});
case WEB3_ERROR:
console.error(action.error)
break;
case ETHEREUM_LOADED:
return Object.assign({}, state, {
loadingWeb3: false,
});
case NETWORK_ID_LOADED:
return Object.assign({}, state, {
networkID: action.id,
});
case LOADING_OWNER:
return Object.assign({}, state, {
loadingOwner: true,
});
case OWNER_LOADED:
return Object.assign({}, state, {
loadingOwner: false,
owner: action.owner,
});
case OWNER_BALANCE_LOADED:
return Object.assign({}, state, {
ownerBalance: action.balance,
});
case KEYCARD_DISCOVERED:
return Object.assign({}, state, {
customer: {
...state.customer,
keycardAddress: action.address,
}
});
case FINDING_WALLET:
return Object.assign({}, state, {
findingWallet: true,
});
case WALLET_FOUND:
return Object.assign({}, state, {
findingWallet: false,
customer: {
...state.customer,
walletAddress: action.address
}
});
case LOADING_WALLET:
return Object.assign({}, state, {
loadingWallet: true,
});
case WALLET_LOADED:
const wallet = {
balance: action.balance,
maxTxValue: action.maxTxValue,
}
return Object.assign({}, state, {
loadingWallet: false,
customer: {
...state.customer,
wallet: wallet,
}
});
case REQUESTING_PAYMENT:
return Object.assign({}, state, {
requestingPayment: true,
});
case PAYMENT_REQUESTED:
return Object.assign({}, state, {
requestingPayment: false,
paymentRequested: true,
});
case PAYMENT_AMOUNT_VALUE_CHANGE:
return Object.assign({}, state, {
txAmount: action.value
});
}
return state;
}

View File

@ -0,0 +1,10 @@
export const emptyAddress = "0x0000000000000000000000000000000000000000"
export const compressedAddress = (a, padding) => {
padding = padding || 4;
return `${a.slice(0, padding + 2)}...${a.slice(a.length - padding)}`
}
export const isEmptyAddress = (a) =>
a == emptyAddress

View File

@ -12,17 +12,26 @@
},
"license": "MIT",
"dependencies": {
"@eth-optimism/solc": "^0.5.16-alpha.2",
"@openzeppelin/cli": "^2.8.2",
"@openzeppelin/contracts-ethereum-package": "^2.5.0",
"eth-sig-util": "^2.5.3",
"minimist": "^1.2.5",
"solc": "0.5.16",
"truffle": "^5.1.44",
"ethers": "^5.0.14"
"truffle": "^5.1.46",
"ethers": "^5.0.14",
"@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.2",
"@material-ui/styles": "^3.0.0-alpha.10",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-redux": "^6.0.1",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"typeface-roboto": "^0.0.54"
},
"devDependencies": {
"@eth-optimism/ovm-toolchain": "^0.0.1-alpha.4",
"@eth-optimism/solc": "^0.5.16-alpha.2",
"@openzeppelin/cli": "^2.8.2",
"@openzeppelin/contracts-ethereum-package": "^2.5.0",
"@eth-optimism/ovm-toolchain": "^0.0.1-alpha.7",
"@openzeppelin/truffle-upgrades": "^1.0.2",
"bip39": "^3.0.2",
"ethereumjs-wallet": "^1.0.0"

File diff suppressed because it is too large Load Diff