From cdc2697640746e8f8c05379eef3b86bc4345007c Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Wed, 8 Apr 2020 21:07:37 -0400 Subject: [PATCH] Add ERC20 widget --- .storybook/src/3.components/erc20-widget.tsx | 63 +++ packages/erc20-widget/.babelrc | 35 ++ packages/erc20-widget/README.md | 9 + packages/erc20-widget/package.json | 69 ++++ packages/erc20-widget/rollup.config.js | 53 +++ .../erc20-widget/src/Erc20Widget.test.tsx | 3 + packages/erc20-widget/src/Erc20Widget.tsx | 359 ++++++++++++++++++ packages/erc20-widget/src/index.ts | 5 + packages/erc20-widget/tsconfig.json | 39 ++ 9 files changed, 635 insertions(+) create mode 100644 .storybook/src/3.components/erc20-widget.tsx create mode 100644 packages/erc20-widget/.babelrc create mode 100644 packages/erc20-widget/README.md create mode 100644 packages/erc20-widget/package.json create mode 100644 packages/erc20-widget/rollup.config.js create mode 100644 packages/erc20-widget/src/Erc20Widget.test.tsx create mode 100644 packages/erc20-widget/src/Erc20Widget.tsx create mode 100644 packages/erc20-widget/src/index.ts create mode 100644 packages/erc20-widget/tsconfig.json diff --git a/.storybook/src/3.components/erc20-widget.tsx b/.storybook/src/3.components/erc20-widget.tsx new file mode 100644 index 0000000..662636b --- /dev/null +++ b/.storybook/src/3.components/erc20-widget.tsx @@ -0,0 +1,63 @@ +import React, {useState} from "react"; +import {storiesOf} from "@storybook/react"; + +import {AxisTheme} from "../../../packages/theme"; +import {Erc20Widget} from "../../../packages/erc20-widget/src"; + +import {Box} from "grommet/es6"; + + +storiesOf("Components | ERC20 Widget", module) + .add("Default", () => { + const Comp = (props) => { + const tokens = { + '0x6b175474e89094c44da98b954eedeac495271d0f':{ + symbol: "DAI", + logo: "https://raw.githubusercontent.com/atomiclabs/cryptocurrency-icons/master/32/color/dai.png", + address: "0x6b175474e89094c44da98b954eedeac495271d0f", + decimals: 12 + }, + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48':{ + symbol: "USDC", + logo: "https://raw.githubusercontent.com/atomiclabs/cryptocurrency-icons/master/32/color/usdc.png", + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimals: 12 + } + } + const token = { + '0x6b175474e89094c44da98b954eedeac495271d0f':{ + symbol: "DAI", + logo: "https://raw.githubusercontent.com/atomiclabs/cryptocurrency-icons/master/32/color/dai.png", + address: "0x6b175474e89094c44da98b954eedeac495271d0f", + decimals: 12 + },} + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + return ; + }) \ No newline at end of file diff --git a/packages/erc20-widget/.babelrc b/packages/erc20-widget/.babelrc new file mode 100644 index 0000000..6edbd3a --- /dev/null +++ b/packages/erc20-widget/.babelrc @@ -0,0 +1,35 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "modules": false + } + ], + "@babel/preset-react", + "@babel/preset-typescript" + + ], + "plugins": [ + "transform-class-properties", + "babel-plugin-styled-components" + ], + "env": { + "test": { + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ], + "@babel/preset-typescript", + "@babel/preset-react" + ], + "plugins": [ + "transform-class-properties", + "babel-plugin-styled-components" + ] + } + } +} diff --git a/packages/erc20-widget/README.md b/packages/erc20-widget/README.md new file mode 100644 index 0000000..2dec693 --- /dev/null +++ b/packages/erc20-widget/README.md @@ -0,0 +1,9 @@ +# `Sample Component` + +> TODO: description + +## Usage + +``` +// TODO: DEMONSTRATE API +``` diff --git a/packages/erc20-widget/package.json b/packages/erc20-widget/package.json new file mode 100644 index 0000000..50ad07e --- /dev/null +++ b/packages/erc20-widget/package.json @@ -0,0 +1,69 @@ +{ + "name": "@centrifuge/erc20-widget", + "version": "0.3.1", + "description": "ERC20 Widget", + "author": "razvan ", + "homepage": "https://github.com/centrifuge/axis/tree/master/packages/axis-sample-component#readme", + "license": "ISC", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "source": "src/index.ts", + "files": [ + "dist", + "src" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/centrifuge/axis.git" + }, + "scripts": { + "build": "rollup -c", + "test": "jest --no-cache --coverage --verbose" + }, + "bugs": { + "url": "https://github.com/centrifuge/axis/issues" + }, + "peerDependencies": { + "grommet": "2.7.x", + "react": ">= 16.6.1", + "react-dom": ">= 16.6.1", + "styled-components": ">= 4.x" + }, + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-transform-typescript": "^7.5.2", + "@babel/preset-env": "^7.5.4", + "@babel/runtime": "^7.3.1", + "@types/react": "^16.8.23", + "@types/react-dom": "^16.8.4", + "@types/styled-components": "^4.1.18", + "babel-jest": "^24.1.0", + "babel-plugin-external-helpers": "^6.22.0", + "babel-plugin-styled-components": "^1.10.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-react-app": "^7.0.2", + "enzyme": "^3.8.0", + "enzyme-adapter-react-16": "^1.9.1", + "jest": "^24.1.0", + "prop-types": "^15.7.2", + "rollup": "^1.1.2", + "rollup-plugin-babel": "^4.3.2", + "rollup-plugin-filesize": "^6.0.1", + "rollup-plugin-node-resolve": "^4.0.0", + "rollup-plugin-peer-deps-external": "^2.2.0", + "rollup-plugin-progress": "^1.0.0", + "rollup-plugin-url": "^2.2.0", + "typescript": "^3.5.2" + }, + "gitHead": "ef53db97aa8cc0b402b3544a731dc10c9ff12641", + "dependencies": { + "@centrifuge/axis-search-select": "^0.3.2-develop.41", + "@centrifuge/axis-theme": "^0.3.1", + "@centrifuge/axis-utils": "^0.3.2-develop.49", + "bignumber.js": "^9.0.0" + } +} diff --git a/packages/erc20-widget/rollup.config.js b/packages/erc20-widget/rollup.config.js new file mode 100644 index 0000000..15d3932 --- /dev/null +++ b/packages/erc20-widget/rollup.config.js @@ -0,0 +1,53 @@ +import babel from "rollup-plugin-babel"; +import filesize from "rollup-plugin-filesize"; +import resolve from "rollup-plugin-node-resolve"; +import progress from "rollup-plugin-progress"; +import peerDepsExternal from "rollup-plugin-peer-deps-external"; +import url from "rollup-plugin-url"; +import { DEFAULT_EXTENSIONS } from '@babel/core'; + +const extensions = [ + ...DEFAULT_EXTENSIONS, + '.ts', + '.tsx' +] + +export default { + input: "src/index.ts", + output: [ + { + exports: "named", + file: "dist/index.cjs.js", + format: "cjs", + sourcemap: true + }, + { + exports: "named", + file: "dist/index.esm.js", + format: "es", + sourcemap: true + } + ], + plugins: [ + + peerDepsExternal(), + progress(), + resolve({ + extensions + }), + url({ + include: ["**/*.woff", "**/*.woff2"], + // setting infinite limit will ensure that the files + // are always bundled with the code, not copied to /dist + limit: Infinity + }), + babel({ + babelrc: true, + extensions + + }), + filesize() + ] +}; + + diff --git a/packages/erc20-widget/src/Erc20Widget.test.tsx b/packages/erc20-widget/src/Erc20Widget.test.tsx new file mode 100644 index 0000000..212802d --- /dev/null +++ b/packages/erc20-widget/src/Erc20Widget.test.tsx @@ -0,0 +1,3 @@ +describe("Sample Component test", () => { + it("needs tests", () => {}); +}); diff --git a/packages/erc20-widget/src/Erc20Widget.tsx b/packages/erc20-widget/src/Erc20Widget.tsx new file mode 100644 index 0000000..47ae776 --- /dev/null +++ b/packages/erc20-widget/src/Erc20Widget.tsx @@ -0,0 +1,359 @@ +import React, { useState, useRef } from "react"; +import styled, { ThemeProps as StyledThemeProps, withTheme } from "styled-components"; +import { MarginType } from "grommet/utils"; +import { defaultProps, extendDefaultTheme } from "grommet/default-props"; +import { Button, Box, Form, FormField, TextInput, Text, Select, Drop, Anchor } from "grommet"; +import { copyToClipboard } from "@centrifuge/axis-utils"; +import bigNumber from "bignumber.js"; +import { AxisTheme } from "@centrifuge/axis-theme"; + +interface ThemeProps { + erc20Widget: { + margin: MarginType + } +} + +interface Props extends StyledThemeProps { + value?: bigNumber | string, + tokenData: TokenMetadata, + balance?: bigNumber | string, + limit?: bigNumber | string, + search?: boolean, + precision?: number, + fieldLabel?: string, + account?: string, + onValueChanged?: (value: string) => void, + errorMessage?: string, + inline? : boolean, + mainFont?: number | string +} + +export interface TokenProps { + symbol: string, + logo: string, + address: string, + decimals: Number +} + +export interface TokenMetadata { + [address:string] : { + name: string, + logo: string, + symbol: string, + decimals: number, + erc20?: boolean + } +} + +const defaultThemeProps: ThemeProps = { + erc20Widget: { + margin: "small" + } +}; + +const overflowStyle = { + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '200px', +}; + +const specialTheme = ({ + select: { + options: { + text : { + weight:'normal', + }, + container : { + align:"start" + } + }, + icons: { + color: "black", + margin: "xsmall", + }, + }, + formField: { + border: { + position: "outer", + color: "none" + }, + margin: { + bottom: "none" + } + }, + anchor :{ + color: 'black', + textDecoration: 'underline', + weight: 'normal', + size: 'small' + } +}); + +const Circleinfo = styled.svg` + fill: black; + & :hover { + path { + fill: black; + } + cursor: pointer; + } +`; + +const copyIcon = () => { + return ( + + + + + ); +} + +export const Erc20Widget: React.FunctionComponent = ( + { + value, + tokenData, + search, + balance, + limit, + theme, + precision, + fieldLabel, + account, + onValueChanged, + errorMessage, + inline, + mainFont + } +) => { + + var tokens: TokenProps[] = []; + for (var k in tokenData) { + tokens.push({'address':k, 'logo':tokenData[k]['logo'], 'decimals':tokenData[k]['decimals'],'symbol':tokenData[k]['symbol']})} + const [amount, setAmount] = useState(value); + const [displayAmount, setDisplayAmount] = useState(''); + const [selectedToken, setToken] = useState((search) ? undefined : tokens[0]); + const [options, setOptions] = useState(tokens); + const [showDrop, setDrop] = useState(false); + const dropRef = useRef(); + + const renderToken = (token) => { + if (token) { + return + + + + + {token.symbol} + + } + else return undefined + } + + const setMax = (value) => { + return ( + + ); + } + + const validateInput = () => { + + if (onValueChanged != undefined) { + onValueChanged(amount?.toString()); + } + + // Check for invalid characters + if (!(/^[0-9,.]*$/.test(displayAmount))) + { + return "Invalid Amount" + } + + // Check for amount with too many decimals of precisions for specified token + try { + if (amount?.toString().replace(/\.?0+$/, "").split('.')[1].length > selectedToken?.decimals) { + return "Invalid Amount" + } + } + catch {} + + // Check if amount is greater than balance + if (amount && balance && (new bigNumber(amount) > balance)) { + if (errorMessage) { + return errorMessage; + } + else return "Invalid Amount" } + + // Check if amount is greater than limit + if (amount && limit && (new bigNumber(amount) > limit)) { + if (errorMessage) { + return errorMessage; + } + else return "Invalid Amount" + } + + if (amount && amount.isNaN()) { + return "Invalid Amount" + } + + } + + const copyAndHighlight = () => { + copyToClipboard((amount) ? amount.toString() : ""); + } + + const updateSearchList = (text) => { + // The line below escapes regular expression special characters: + // [ \ ^ $ . | ? * + ( ) + const escapedText = text.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); + const exp = new RegExp(escapedText, "i"); + setOptions(options.filter(o => exp.test(o.symbol))); + } + + const renderAddress = () => { + return ( + + Account: {account} + copyToClipboard(account)}>{copyIcon()} + + ) + } + + const renderDisplayAmount = (newAmount : string) => { + if (newAmount == "NaN" || newAmount == ""){ + setDisplayAmount(""); + setAmount(new bigNumber(0)); + } + else if (!(/^[0-9,.]*$/.test(newAmount))){ + setDisplayAmount(newAmount); + } + else if ((newAmount[newAmount.length-1] == '.') || (newAmount[newAmount.length-1] == '0')){ + setDisplayAmount(newAmount); + } + else { + var newValue = newAmount.replace(/,/g,''); + setAmount(new bigNumber(newValue)); + setDisplayAmount((new bigNumber(newValue)).toFormat()); + } + } + + + return ( + + 1 ? "336px" : "284px"}}> + { /* Optional Field Label and Information Icon */} + + {!inline && + {fieldLabel} + (selectedToken ? setDrop(true) : undefined)} + width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> + + + + + {showDrop && setDrop(false)} + target={dropRef.current} + align={{ right: "right" }} + > + {account && ERC20 Token Balance} + {account && renderAddress()} + Token: {selectedToken?.symbol} +  on Etherscan + + } + } + + + + + { /* Input Field for Token Balance */} + {!value &&
+ validateInput()}> + renderDisplayAmount(event.target.value) + } + /> + +
} + + { /* Amount Display for Token Balance */} + {value && + { + if (event.detail == 2) { + copyAndHighlight(); + } + }}> + + {(precision) ? new bigNumber(value).toFormat(precision) + + ((value.toString().includes('.') && (value.toString().split('.')[1].length > precision)) ? '…' : '') + : new bigNumber(value).toFormat()} + + } + + + { /* Token Icon/Ticker Display */} + {tokens.length == 1 && + {renderToken(selectedToken)} + } + + { /* Token Drop-down if multiple tokens specified */} + {tokens.length > 1 && +