From 232c364e88436517a5a7a7dca3b752f5375960c8 Mon Sep 17 00:00:00 2001 From: mtsalenc Date: Wed, 6 Jan 2021 03:49:28 -0300 Subject: [PATCH] feat: add token and tx hash forwarding --- .env | 3 +- .eslintignore | 3 +- .eslintrc.js | 6 +- .gitignore | 2 + .gitignore copy | 36 -------- .prettierignore | 7 +- package.json | 4 +- src/abis/erc20.json | 172 ++++++++++++++++++++++++++++++++++++++ src/app.tsx | 48 +++++++++-- src/hooks/socket.tsx | 5 +- src/hooks/wallet.tsx | 21 ++++- src/index.tsx | 2 +- src/report-web-vitals.ts | 18 ++++ src/reportWebVitals.ts | 15 ---- src/types/global.d.ts | 10 +++ src/utils/erc20-tokens.ts | 12 +++ src/utils/erc20.ts | 5 -- src/utils/index.ts | 2 +- 18 files changed, 294 insertions(+), 77 deletions(-) delete mode 100644 .gitignore copy create mode 100644 src/abis/erc20.json create mode 100644 src/report-web-vitals.ts delete mode 100644 src/reportWebVitals.ts create mode 100644 src/utils/erc20-tokens.ts delete mode 100644 src/utils/erc20.ts diff --git a/.env b/.env index be454c6..17b1140 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -NEXT_PUBLIC_BACKEND_URL=localhost:3001/pay \ No newline at end of file +REACT_APP_BACKEND_URL=localhost:3001/pay +REACT_APP_TARGET_WALLET=0xd84c2518d25c158777feef141354e8bbf3d73cdd \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 62d2ce2..dcda90f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules .next -.vscode \ No newline at end of file +.vscode +build \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 7ff984f..790fba4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { rules: { // We will use TypeScript's types for component props instead 'react/prop-types': 'off', - '@typescript-eslint/no-unused-vars': ['error'], + '@typescript-eslint/no-unused-vars': ['warn'], 'unicorn/no-useless-undefined': 0, '@typescript-eslint/explicit-function-return-type': 0, @@ -57,7 +57,7 @@ module.exports = { 'no-useless-concat': 2, 'prefer-template': 2, - 'prettier/prettier': ['error', {}, { usePrettierrc: true }], // Includes .prettierrc.js rules + 'prettier/prettier': ['warn', {}, { usePrettierrc: true }], // Includes .prettierrc.js rules }, }, ], @@ -88,7 +88,7 @@ module.exports = { 'react/destructuring-assignment': [2, 'always'], // hooks - 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/rules-of-hooks': 'warn', 'react-hooks/exhaustive-deps': 'warn', }, }; diff --git a/.gitignore b/.gitignore index 4d29575..e137dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.eslintcache \ No newline at end of file diff --git a/.gitignore copy b/.gitignore copy deleted file mode 100644 index e7bb9b0..0000000 --- a/.gitignore copy +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel - -.env.local \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index a581f9b..50553fb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,9 @@ node_modules .vscode package-lock.json -package.json \ No newline at end of file +package.json +build +.env +.eslintignore +.gitignore +.prettierignore \ No newline at end of file diff --git a/package.json b/package.json index 3be595a..0c2000c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "start": "react-scripts start", - "build": "react-scripts build", + "build": "GENERATE_SOURCEMAP=false react-scripts build", "lint:eslint": "eslint --ext js,jsx,ts,tsx", "lint:eslint:fix": "eslint --ext js,jsx,ts,tsx --fix", "lint:prettier": "prettier --config .prettierrc.js . --check", @@ -86,4 +86,4 @@ "volta": { "node": "12.20.0" } -} +} \ No newline at end of file diff --git a/src/abis/erc20.json b/src/abis/erc20.json new file mode 100644 index 0000000..5044102 --- /dev/null +++ b/src/abis/erc20.json @@ -0,0 +1,172 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + }, + { + "internalType": "address", + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "remaining", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_from", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/app.tsx b/src/app.tsx index 6417339..77f677d 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,38 +1,70 @@ import { PageContent, Input, Button, Stack, Card, Flex } from 'bumbag'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import { ethers } from 'ethers'; import { useWallet } from './hooks'; import useSocket from './hooks/socket'; +import { ERC20Tokens } from './utils'; +import erc20Abi from './abis/erc20.json'; const App = (): JSX.Element => { - const { library, account, onConnectWallet, active } = useWallet(); + const { + account, + onConnectWallet, + active, + chainId, + uncheckedSigner, + library, + } = useWallet(); const { connection } = useSocket({ onMessageReceived: useCallback(() => undefined, []), }); + const tokens = useMemo(() => chainId && ERC20Tokens[chainId], [chainId]); + const tokenContracts = useMemo(() => { + if (typeof chainId === 'undefined' || !uncheckedSigner || !tokens) return; + return Object.values(ERC20Tokens[chainId]).map( + ({ address }) => new ethers.Contract(address, erc20Abi, uncheckedSigner) + ); + }, [chainId, tokens, uncheckedSigner]); + const onRequestPay = useCallback(() => { - if (!library || !account || !connection) return; + if ( + !uncheckedSigner || + !account || + !tokenContracts || + !chainId || + !tokens || + !connection + ) + return; (async () => { try { - const signer = library.getSigner(account); - const signature = await signer.signMessage('👋'); + // For now, only one token is supported. + const tokenContract = tokenContracts[0]; + + console.info('requesting signature...'); + const tx = await tokenContract.transfer( + process.env.REACT_APP_TARGET_WALLET, + 1 + ); connection.send( JSON.stringify({ - signature, + txHash: tx.hash, qrCode: 'Here it comes!', }) ); - console.info('Message sent'); } catch (error) { console.error( `Failure!${error && error.message ? `\n\n${error.message}` : ''}` ); } })(); - }, [library, account, connection]); + }, [account, chainId, connection, tokenContracts, tokens, uncheckedSigner]); return ( + {!tokens && 'Network not supported.'} diff --git a/src/hooks/socket.tsx b/src/hooks/socket.tsx index 7c8190c..9f0ce8d 100644 --- a/src/hooks/socket.tsx +++ b/src/hooks/socket.tsx @@ -15,9 +15,10 @@ export default function useSocket({ const [error, setError] = useState(); useEffect(() => { + if (error) return; console.info('Attempting to stablish connection...'); const ws = - new window.WebSocket(`ws://${process.env.NEXT_PUBLIC_BACKEND_URL}`) || {}; + new window.WebSocket(`ws://${process.env.REACT_APP_BACKEND_URL}`) || {}; ws.addEventListener('open', () => { console.info(`Connection opened`); @@ -44,7 +45,7 @@ export default function useSocket({ setConnection(undefined); onConnectionClosed(event); }); - }, [onConnectionClosed, onConnectionOpened, onMessageReceived]); + }, [error, onConnectionClosed, onConnectionOpened, onMessageReceived]); return { connection, diff --git a/src/hooks/wallet.tsx b/src/hooks/wallet.tsx index e4347ac..df7e7e5 100644 --- a/src/hooks/wallet.tsx +++ b/src/hooks/wallet.tsx @@ -1,10 +1,12 @@ import { useWeb3React } from '@web3-react/core'; import { useCallback, useEffect, useState } from 'react'; import { AbstractConnector } from '@web3-react/abstract-connector'; +import { ethers } from 'ethers'; import useEagerConnect from './eager-connect'; import useInactiveListener from './inactive-listener'; import { injected } from '../connectors'; import { Web3ReactContextInterface } from '@web3-react/core/dist/types'; +import { UncheckedJsonRpcSigner } from '../types/global'; enum ConnectorNames { Injected = 'Injected', @@ -18,12 +20,17 @@ const connectorsByName: { interface Properties extends Web3ReactContextInterface { onConnectWallet: () => void; + uncheckedSigner?: UncheckedJsonRpcSigner; } // Requires web3-react in the context. const useWallet = (): Properties => { const web3ReactContext = useWeb3React(); - const { activate, connector } = web3ReactContext; + const { activate, connector, account } = web3ReactContext; + const library: ethers.providers.JsonRpcProvider = web3ReactContext.library; + const [uncheckedSigner, setUncheckedSigner] = useState< + undefined | UncheckedJsonRpcSigner + >(); // Handle logic to recognize the connector currently being activated. const [activatingConnector, setActivatingConnector] = useState(); @@ -38,6 +45,17 @@ const useWallet = (): Properties => { // Handle logic to connect in reaction to certain events on the injected ethereum provider, if it exists. useInactiveListener(!triedEager || !!activatingConnector); + useEffect(() => { + if (!library || !account) return; + (async () => { + try { + setUncheckedSigner(await library.getUncheckedSigner()); + } catch { + console.error('Error getting signer'); + } + })(); + }, [account, library]); + const onConnectWallet = useCallback(() => { setActivatingConnector(connectorsByName[ConnectorNames.Injected]); activate(connectorsByName[ConnectorNames.Injected]); @@ -46,6 +64,7 @@ const useWallet = (): Properties => { return { ...web3ReactContext, onConnectWallet, + uncheckedSigner, }; }; diff --git a/src/index.tsx b/src/index.tsx index da26593..476c5d8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ import { JsonRpcFetchFunc, } from '@ethersproject/providers'; import App from './app'; -import reportWebVitals from './reportWebVitals'; +import reportWebVitals from './report-web-vitals'; const getLibrary = ( provider: ExternalProvider | JsonRpcFetchFunc diff --git a/src/report-web-vitals.ts b/src/report-web-vitals.ts new file mode 100644 index 0000000..34d9023 --- /dev/null +++ b/src/report-web-vitals.ts @@ -0,0 +1,18 @@ +import { ReportHandler } from 'web-vitals'; + +const reportWebVitals = (onPerfEntry?: ReportHandler) => { + if (onPerfEntry && onPerfEntry instanceof Function) + import('web-vitals') + .then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + + return; + }) + .catch((error) => console.error(error)); +}; + +export default reportWebVitals; diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts deleted file mode 100644 index 49a2a16..0000000 --- a/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReportHandler } from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 2de6857..d91d6f5 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,5 +1,15 @@ +import { ethers } from 'ethers'; + +/* eslint-disable unicorn/prevent-abbreviations */ declare namespace NodeJS { export interface ProcessEnv { REACT_APP_BACKEND_URL: string; + REACT_APP_TARGET_WALLET: string; } } + +declare class UncheckedJsonRpcSigner extends ethers.providers.JsonRpcSigner { + sendTransaction( + transaction: Deferrable + ): Promise; +} diff --git a/src/utils/erc20-tokens.ts b/src/utils/erc20-tokens.ts new file mode 100644 index 0000000..19fc164 --- /dev/null +++ b/src/utils/erc20-tokens.ts @@ -0,0 +1,12 @@ +const ERC20Tokens: { + [key: number]: { [key: string]: { address: string; decimals: number } }; +} = { + 5: { + '0xD7e4fB1ba9F6811Ba5503F1e94f23bCc967670C7': { + address: '0xD7e4fB1ba9F6811Ba5503F1e94f23bCc967670C7', + decimals: 18, + }, + }, +}; + +export default ERC20Tokens; diff --git a/src/utils/erc20.ts b/src/utils/erc20.ts deleted file mode 100644 index 8bd2d22..0000000 --- a/src/utils/erc20.ts +++ /dev/null @@ -1,5 +0,0 @@ -const acceptedErc20Tokens = { - 4: ['0xd7e4fb1ba9f6811ba5503f1e94f23bcc967670c7'], -}; - -export default acceptedErc20Tokens; diff --git a/src/utils/index.ts b/src/utils/index.ts index 0c0aad2..a9fc4b6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1 @@ -export { default as Networks } from './erc20'; +export { default as ERC20Tokens } from './erc20-tokens';