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 && }
+
+ { /* 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 &&
+ }
+
+
+ { /* Balance/Limit if specified */}
+
+ {(balance || limit) && !inline &&
+ {balance ? Balance : {new bigNumber(balance).toFormat()} :
+ Limit : {new bigNumber(limit).toFormat()}}
+ {balance ? setMax(balance) : setMax(limit)}
+ }
+
+
+
+ );
+};
+
+extendDefaultTheme(defaultThemeProps);
+
+Erc20Widget.defaultProps = {
+ tokens: [
+ {
+ symbol: "DAI",
+ logo: "",
+ address: "0x6b175474e89094c44da98b954eedeac495271d0f",
+ decimals: 12
+ }
+ ],
+ inline: false,
+ ...defaultProps
+};
+
+export default withTheme(Erc20Widget)
diff --git a/packages/erc20-widget/src/index.ts b/packages/erc20-widget/src/index.ts
new file mode 100644
index 0000000..b6e7b96
--- /dev/null
+++ b/packages/erc20-widget/src/index.ts
@@ -0,0 +1,5 @@
+import Erc20Widget from "./Erc20Widget";
+
+export {
+ Erc20Widget
+}
diff --git a/packages/erc20-widget/tsconfig.json b/packages/erc20-widget/tsconfig.json
new file mode 100644
index 0000000..d21dd26
--- /dev/null
+++ b/packages/erc20-widget/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "module": "esnext",
+ "noImplicitAny": false,
+ "removeComments": true,
+ "noLib": false,
+ "allowSyntheticDefaultImports": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "target": "es6",
+ "sourceMap": true,
+ "outDir": "./dist",
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "isolatedModules": false,
+ "strictPropertyInitialization": false,
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ]
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts"
+ ]
+}