add buckets list page

This commit is contained in:
Andrea Franz 2020-09-23 17:32:54 +02:00
parent 216fa8aed8
commit de05b94699
No known key found for this signature in database
GPG Key ID: 4F0D2F2D9DE7F29D
14 changed files with 431 additions and 25 deletions

View File

@ -0,0 +1,134 @@
import { RootState } from '../reducers';
import { Dispatch } from 'redux';
import { bucketsAddresses } from "../config";
import Bucket from '../contracts/Bucket.json';
import { newBucketContract } from "../utils";
import { TokenType } from "../reducers/buckets";
import IERC20Detailed from '../contracts/IERC20Detailed.json';
import { AbiItem } from "web3-utils";
import { config } from "../config";
import { TokenDetails, ERC20Details } from "../reducers/buckets";
export const BUCKETS_LOADING = "BUCKETS_LOADING";
export interface BucketsLoadingAction {
type: typeof BUCKETS_LOADING
recipientAddress: string
}
export const BUCKETS_LOADING_BUCKET = "BUCKETS_LOADING_BUCKET";
export interface BucketsLoadingBucketAction {
type: typeof BUCKETS_LOADING_BUCKET
recipientAddress: string
bucketAddress: string
}
export const BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED = "BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED";
export interface BucketsRedeemableTokenAddressLoadedAction {
type: typeof BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED
recipientAddress: string
bucketAddress: string
tokenAddress: string
}
export const BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED = "BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED";
export interface BucketsRedeemableTokenTypeLoadedAction {
type: typeof BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED
recipientAddress: string
bucketAddress: string
tokenType: TokenType
}
export const BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED = "BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED";
export interface BucketsRedeemableTokenDetailsLoadedAction {
type: typeof BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED
recipientAddress: string
bucketAddress: string
tokenDetails: TokenDetails
}
export type BucketsActions =
BucketsLoadingAction |
BucketsLoadingBucketAction |
BucketsRedeemableTokenAddressLoadedAction |
BucketsRedeemableTokenTypeLoadedAction |
BucketsRedeemableTokenDetailsLoadedAction;
export const loadingBuckets = (recipientAddress: string): BucketsLoadingAction => ({
type: BUCKETS_LOADING,
recipientAddress,
});
export const loadingBucket = (recipientAddress: string, bucketAddress: string): BucketsLoadingBucketAction => ({
type: BUCKETS_LOADING_BUCKET,
recipientAddress,
bucketAddress,
});
export const tokenAddressLoaded = (recipientAddress: string, bucketAddress: string, tokenAddress: string): BucketsRedeemableTokenAddressLoadedAction => ({
type: BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED,
recipientAddress,
bucketAddress,
tokenAddress,
});
export const tokenTypeLoaded = (recipientAddress: string, bucketAddress: string, tokenType: TokenType): BucketsRedeemableTokenTypeLoadedAction => ({
type: BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED,
recipientAddress,
bucketAddress,
tokenType,
});
export const tokenDetailsLoaded = (recipientAddress: string, bucketAddress: string, tokenDetails: TokenDetails): BucketsRedeemableTokenDetailsLoadedAction => ({
type: BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED,
recipientAddress,
bucketAddress,
tokenDetails,
});
export const loadBuckets = (recipientAddress: string) => {
return (dispatch: Dispatch, getState: () => RootState) => {
dispatch(loadingBuckets(recipientAddress));
const addresses = bucketsAddresses();
addresses.forEach((bucketAddress) => {
dispatch<any>(loadBucket(recipientAddress, bucketAddress));
});
}
}
const loadBucket = (recipientAddress: string, bucketAddress: string) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(loadingBucket(recipientAddress, bucketAddress));
const bucket = newBucketContract(bucketAddress);
const _type = await bucket.methods.bucketType().call();
const type = _type === "20" ? "erc20" : "ntf";
dispatch(tokenTypeLoaded(recipientAddress, bucketAddress, type as TokenType));
const tokenAddress = await bucket.methods.tokenAddress().call();
dispatch(tokenAddressLoaded(recipientAddress, bucketAddress, tokenAddress));
//FIXME: catch possible error
const tokenDetails = await loadERC20Token(tokenAddress);
dispatch(tokenDetailsLoaded(recipientAddress, bucketAddress, tokenDetails));
}
}
const loadERC20Token = (address: string): Promise<ERC20Details> => {
return new Promise(async (resolve, reject) => {
const erc20Abi = IERC20Detailed.abi as AbiItem[];
const erc20 = new config.web3!.eth.Contract(erc20Abi, address);
try {
const name = await erc20.methods.name().call();
const symbol = await erc20.methods.symbol().call();
const decimals = parseInt(await erc20.methods.decimals().call());
resolve({
name,
symbol,
decimals,
});
} catch(e) {
reject(e);
}
});
}

View File

@ -1,11 +1,10 @@
import { RootState } from '../reducers';
import { config } from "../config";
import { Dispatch } from 'redux';
import { newBucketContract } from "./redeemable";
import { sha3 } from "web3-utils";
import { recoverTypedSignature } from 'eth-sig-util';
import { Web3Type } from "../actions/web3";
import { KECCAK_EMPTY_STRING } from '../utils';
import { KECCAK_EMPTY_STRING, newBucketContract} from '../utils';
import { debug } from "./debug";
interface RedeemMessage {

View File

@ -2,14 +2,16 @@ import { RootState } from '../reducers';
import ERC20BucketFactory from '../contracts/ERC20BucketFactory.json';
import NFTBucketFactory from '../contracts/NFTBucketFactory.json';
import ERC20Bucket from '../contracts/ERC20Bucket.json';
import Bucket from '../contracts/Bucket.json';
import IERC20Detailed from '../contracts/IERC20Detailed.json';
import IERC721Metadata from '../contracts/IERC721Metadata.json';
import { config } from "../config";
import { Dispatch } from 'redux';
import { ZERO_ADDRESS } from "../utils";
import { debug } from "./debug";
import { AbiItem } from "web3-utils";
import {
ZERO_ADDRESS ,
newBucketContract,
} from "../utils";
import { debug } from "./debug";
interface ContractSpecs {
networks: {
@ -178,18 +180,6 @@ export const tokenMetadataLoaded = (tokenAddress: string, recipient: string, met
metadata,
});
export const newBucketContract = (address: string) => {
const bucketAbi = Bucket.abi as AbiItem[];
const bucket = new config.web3!.eth.Contract(bucketAbi, address);
return bucket;
}
export const newERC20BucketContract = (address: string) => {
const bucketAbi = ERC20Bucket.abi as AbiItem[];
const bucket = new config.web3!.eth.Contract(bucketAbi, address);
return bucket;
}
export const loadRedeemable = (bucketAddress: string, recipientAddress: string) => {
return async (dispatch: Dispatch, getState: () => RootState) => {
const networkID = getState().web3.networkID!;

View File

@ -0,0 +1,92 @@
import React, { useEffect } from 'react';
import { RootState } from '../reducers';
import { useRouteMatch } from 'react-router-dom';
import {
shallowEqual,
useSelector,
useDispatch,
} from 'react-redux';
import { recipientBucketsPath } from '../config';
import { loadBuckets } from "../actions/buckets";
import { ERC20Details } from "../reducers/buckets";
interface BuckestListItemProps {
bucketAddress: string
}
const BuckestListItem = (ownProps: BuckestListItemProps) => {
const props = useSelector((state: RootState) => {
const redeemable = state.buckets.redeemables[ownProps.bucketAddress];
if (redeemable === undefined) {
return null;
}
return {
bucketAddress: ownProps.bucketAddress,
loading: redeemable.loading,
tokenAddress: redeemable.tokenAddress,
tokenType: redeemable.tokenType,
tokenDetails: redeemable.tokenDetails,
}
}, shallowEqual);
if (props === null) {
return null;
}
return <div>
Bucket: {props.bucketAddress}
{props.loading && <span> (LOADING...)</span>}
<br />
Token address: {props.tokenAddress}
<br />
Type: {props.tokenType}
<br />
{props.tokenDetails && <>
Symbol: {props.tokenDetails.symbol}
<br />
Name: {props.tokenDetails.name}
</>}
<hr />
</div>;
}
interface URLParams {
recipientAddress: string
}
export default function(ownProps: any) {
const dispatch = useDispatch()
const match = useRouteMatch<URLParams>({
path: recipientBucketsPath,
exact: true,
});
if (match === null) {
return null;
}
const recipientAddress = match.params.recipientAddress;
const props = useSelector((state: RootState) => {
return {
loading: state.buckets.loading,
buckets: state.buckets.buckets,
}
}, shallowEqual);
useEffect(() => {
console.log("loading buckets")
dispatch(loadBuckets(recipientAddress));
}, [dispatch, recipientAddress]); // FIXME: unload buckets
return <div>
<div>buckets for {recipientAddress}</div>
<ul>
{props.buckets.map(bucketAddress => <li key={bucketAddress}>
<BuckestListItem bucketAddress={bucketAddress} />
</li>)}
</ul>
</div>;
}

View File

@ -40,7 +40,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'
const buckerErrorMessage = (error: RedeemableErrors): string => {
const bucketErrorMessage = (error: RedeemableErrors): string => {
switch (error.type) {
case ERROR_LOADING_REDEEMABLE:
return "couldn't load redeemable";
@ -116,7 +116,7 @@ export default function(ownProps: any) {
if (props.error) {
return <div className={classNames({ paper: true, error: true })}>
{buckerErrorMessage(props.error)}
{bucketErrorMessage(props.error)}
</div>;
}

View File

@ -8,4 +8,14 @@ export const config: Config = {
web3: undefined
};
export const recipientBucketsPath = "/recipients/:recipientAddress/buckets";
export const redeemablePath = "/buckets/:bucketAddress/redeemables/:recipientAddress";
export const bucketsAddresses = (): Array<string> => {
const s = process.env.REACT_APP_BUCKETS;
if (s === undefined) {
return [];
}
return s.split(",").map((a) => a.trim());
}

View File

@ -12,7 +12,11 @@ import ErrorBoundary from './components/ErrorBoundary';
import Layout from './components/Layout';
import Home from './components/Home';
import Redeemable from './components/Redeemable';
import { redeemablePath } from './config';
import RecipientBuckets from './components/RecipientBuckets';
import {
recipientBucketsPath,
redeemablePath,
} from './config';
const logger: Middleware = ({ getState }: MiddlewareAPI) => (next: Dispatch) => action => {
console.log('dispatch', action);
@ -49,6 +53,7 @@ ReactDOM.render(
<ConnectedRouter history={history}>
<Switch>
<Route exact path="/"><Home /></Route>
<Route exact path={recipientBucketsPath}><RecipientBuckets /></Route>
<Route exact path={redeemablePath}><Redeemable /></Route>
<Route render={() => "page not found"} />
</Switch>

View File

@ -0,0 +1,155 @@
import {
BucketsActions,
BUCKETS_LOADING,
BUCKETS_LOADING_BUCKET,
BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED,
BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED,
BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED,
} from '../actions/buckets';
export type TokenType = "erc20" | "nft";
export interface ERC20Details {
name: string
symbol: string
decimals: number
}
export type TokenDetails = ERC20Details;
interface Redeemable {
bucketAddress: string
tokenAddress: undefined | string
loading: boolean
tokenType: undefined | TokenType
tokenDetails: undefined | ERC20Details
value: undefined | string
}
export interface BucketsState {
recipientAddress: undefined | string
loading: boolean
buckets: Array<string>
redeemables: {
[bucketAddress: string]: Redeemable
}
}
const initialRedeemableState = {
bucketAddress: "",
tokenAddress: undefined,
loading: true,
tokenType: undefined,
tokenDetails: undefined,
value: undefined
}
const initialState: BucketsState = {
recipientAddress: undefined,
loading: false,
buckets: [],
redeemables: {},
};
export const bucketsReducer = (state: BucketsState = initialState, action: BucketsActions): BucketsState => {
switch (action.type) {
case BUCKETS_LOADING: {
return {
...initialState,
recipientAddress: action.recipientAddress,
loading: true,
};
}
case BUCKETS_LOADING_BUCKET: {
if (action.recipientAddress !== state.recipientAddress) {
return state;
}
return {
...state,
buckets: [
...state.buckets,
action.bucketAddress,
],
redeemables: {
...state.redeemables,
[action.bucketAddress]: {
...initialRedeemableState,
bucketAddress: action.bucketAddress,
loading: true,
}
},
};
}
case BUCKETS_REDEEMABLE_TOKEN_ADDRESS_LOADED: {
if (action.recipientAddress !== state.recipientAddress) {
return state;
}
const redeemable = state.redeemables[action.bucketAddress];
if (redeemable === undefined) {
return state;
}
return {
...state,
redeemables: {
...state.redeemables,
[action.bucketAddress]: {
...redeemable,
tokenAddress: action.tokenAddress,
}
},
};
}
case BUCKETS_REDEEMABLE_TOKEN_TYPE_LOADED: {
if (action.recipientAddress !== state.recipientAddress) {
return state;
}
const redeemable = state.redeemables[action.bucketAddress];
if (redeemable === undefined) {
return state;
}
return {
...state,
redeemables: {
...state.redeemables,
[action.bucketAddress]: {
...redeemable,
tokenType: action.tokenType,
}
},
};
}
case BUCKETS_REDEEMABLE_TOKEN_DETAILS_LOADED: {
if (action.recipientAddress !== state.recipientAddress) {
return state;
}
const redeemable = state.redeemables[action.bucketAddress];
if (redeemable === undefined) {
return state;
}
return {
...state,
redeemables: {
...state.redeemables,
[action.bucketAddress]: {
...redeemable,
tokenDetails: action.tokenDetails,
}
},
};
}
default:
return state;
}
}

View File

@ -21,6 +21,10 @@ import {
DebugState,
debugReducer,
} from './debug';
import {
BucketsState,
bucketsReducer,
} from './buckets';
export interface RootState {
web3: Web3State,
@ -28,6 +32,7 @@ export interface RootState {
redeem: RedeemState,
layout: LayoutState,
debug: DebugState,
buckets: BucketsState,
}
export default function(history: History) {
@ -38,5 +43,6 @@ export default function(history: History) {
redeem: redeemReducer,
layout: layoutReducer,
debug: debugReducer,
buckets: bucketsReducer,
});
}

View File

@ -4,6 +4,9 @@ import {
Token,
TokenERC20,
} from "./actions/redeemable";
import { AbiItem } from "web3-utils";
import Bucket from './contracts/Bucket.json';
import { config } from "./config";
// keccak256("")
export const KECCAK_EMPTY_STRING = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
@ -30,3 +33,9 @@ export const isTokenERC20 = (token: Token): token is TokenERC20 => {
export const compressAddress = (a: string, padding: number = 4) => {
return `${a.slice(0, padding + 2)}...${a.slice(a.length - padding)}`;
}
export const newBucketContract = (address: string) => {
const bucketAbi = Bucket.abi as AbiItem[];
const bucket = new config.web3!.eth.Contract(bucketAbi, address);
return bucket;
}

View File

@ -3,6 +3,7 @@ pragma solidity ^0.5.16;
import "./IERC20.sol";
contract IERC20Detailed is IERC20 {
function name() public view returns (string memory);
function symbol() public view returns (string memory);
function decimals() public view returns (uint8);
}

View File

@ -6,10 +6,12 @@ import "./StandardToken.sol";
* @notice ERC20Token for test scripts, can be minted by anyone.
*/
contract TestToken is StandardToken {
string private _name;
string private _symbol;
uint256 private _decimals;
constructor(string memory symbol, uint256 decimals) public {
constructor(string memory name, string memory symbol, uint256 decimals) public {
_name = name;
_symbol = symbol;
_decimals = decimals;
}
@ -20,6 +22,10 @@ contract TestToken is StandardToken {
mint(amount * uint256(10)**_decimals);
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}

View File

@ -9,6 +9,6 @@ module.exports = function(deployer, network) {
deployer.deploy(ERC20BucketFactory);
if (network === "development") {
deployer.deploy(TestToken, "TEST", 18);
deployer.deploy(TestToken, "Dev Test Token", "DTT", 18);
}
};

View File

@ -25,8 +25,7 @@ const KECCAK_EMPTY_STRING = "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b
const KECCAK_EMPTY_STRING2 = web3.utils.sha3(KECCAK_EMPTY_STRING);
async function deployFactory() {
let code = "0x" + BucketFactoryCode;
let methodCall = BucketFactory.deploy({data: code});
let methodCall = BucketFactory.deploy({data: BucketFactoryCode});
let receipt = await account.sendMethod(methodCall, null);
return receipt.contractAddress;
}
@ -191,7 +190,7 @@ async function run() {
for (let keycard of keycards) {
const create = argv["nft"] ? transferNFT : createRedeemable;
await create(keycard);
console.log(`http://test-pn.keycard.cash/redeem/#/buckets/${bucket}/redeemables/${keycard.keycard}`)
console.log(`http://localhost:3000/redeem/#/buckets/${bucket}/redeemables/${keycard.keycard}`)
}
} else if (!hasDoneSomething) {
console.error("the --file option must be specified");