mirror of
https://github.com/kremalicious/blog.git
synced 2024-12-22 17:23:50 +01:00
Merge pull request #195 from kremalicious/feature/web3-refactor
web3 refactor
This commit is contained in:
commit
a9c1e30e6e
39
.github/workflows/test.yml
vendored
39
.github/workflows/test.yml
vendored
@ -1,39 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: ['10', '12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: ${{ runner.os }}-node-${{ matrix.node-version }}
|
||||
|
||||
- name: Cache Gatsby build output
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: public
|
||||
key: ${{ runner.os }}-public
|
||||
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
env:
|
||||
CI: true
|
||||
GATSBY_GITHUB_TOKEN: ${{ secrets.GATSBY_GITHUB_TOKEN }}
|
@ -121,6 +121,12 @@ exports.onPostBuild = async ({ graphql }) => {
|
||||
// https://github.com/ethereum/web3.js/issues/1105#issuecomment-446039296
|
||||
exports.onCreateWebpackConfig = ({ actions }) => {
|
||||
actions.setWebpackConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
// replace native `scrypt` module with pure js `js-scrypt`
|
||||
scrypt: 'js-scrypt'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
// ignore these plugins completely
|
||||
new webpack.IgnorePlugin(/^(?:electron|ws)$/)
|
||||
|
2989
package-lock.json
generated
2989
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -29,10 +29,15 @@
|
||||
"not op_mini all"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ethersproject/providers": "^5.0.0-beta.146",
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@loadable/component": "^5.10.3",
|
||||
"@web3-react/core": "^6.0.0-beta.15",
|
||||
"@web3-react/injected-connector": "^6.0.0-beta.17",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.8.1",
|
||||
"dms2dec": "^1.1.0",
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"fast-exif": "^1.0.1",
|
||||
"feather-icons": "^4.24.1",
|
||||
"fraction.js": "^4.0.12",
|
||||
@ -58,36 +63,33 @@
|
||||
"gatsby-remark-copy-linked-files": "^2.1.30",
|
||||
"gatsby-remark-images": "^3.1.33",
|
||||
"gatsby-remark-smartypants": "^2.1.16",
|
||||
"gatsby-remark-vscode": "^1.3.0",
|
||||
"gatsby-remark-vscode": "^1.4.0",
|
||||
"gatsby-source-filesystem": "^2.1.38",
|
||||
"gatsby-source-graphql": "^2.1.24",
|
||||
"gatsby-transformer-remark": "^2.6.37",
|
||||
"gatsby-transformer-sharp": "^2.3.5",
|
||||
"graphql": "^14.5.8",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"load-script": "^1.0.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"pigeon-maps": "^0.14.0",
|
||||
"pigeon-marker": "^0.3.4",
|
||||
"react": "^16.12.0",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-clipboard.js": "^2.0.13",
|
||||
"react-clipboard.js": "^2.0.16",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-feather": "^2.0.3",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-pose": "^4.0.9",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-qr-svg": "^2.2.1",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"remark": "^11.0.1",
|
||||
"remark": "^11.0.2",
|
||||
"remark-react": "^6.0.0",
|
||||
"slugify": "^1.3.6",
|
||||
"use-dark-mode": "^2.3.1",
|
||||
"web3": "^1.2.4"
|
||||
"use-dark-mode": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/node": "^7.7.0",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-typescript": "^7.7.0",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@svgr/webpack": "^4.3.3",
|
||||
"@testing-library/jest-dom": "^4.2.3",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
@ -95,11 +97,11 @@
|
||||
"@types/jest": "^24.0.21",
|
||||
"@types/loadable__component": "^5.10.0",
|
||||
"@types/lunr": "^2.3.2",
|
||||
"@types/node": "^12.12.11",
|
||||
"@types/react": "^16.9.11",
|
||||
"@types/node": "^12.12.12",
|
||||
"@types/node-fetch": "^2.5.4",
|
||||
"@types/react": "^16.9.12",
|
||||
"@types/react-dom": "^16.9.3",
|
||||
"@types/react-helmet": "^5.0.14",
|
||||
"@types/react-modal": "^3.10.0",
|
||||
"@types/react-transition-group": "^4.2.3",
|
||||
"@types/shortid": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.8.0",
|
||||
@ -107,7 +109,7 @@
|
||||
"@welldone-software/why-did-you-render": "^3.3.9",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint": "^6.7.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-loader": "^3.0.2",
|
||||
"eslint-plugin-graphql": "^3.1.0",
|
||||
|
4
src/@types/node_modules.d.ts
vendored
4
src/@types/node_modules.d.ts
vendored
@ -2,3 +2,7 @@ declare module 'pigeon-maps'
|
||||
declare module 'pigeon-marker'
|
||||
declare module 'react-blockies'
|
||||
declare module 'remark-react'
|
||||
|
||||
declare module 'ethereum-blockies' {
|
||||
export function toDataUrl(address: string): string
|
||||
}
|
||||
|
@ -28,33 +28,33 @@ function FeaturedPure({
|
||||
)
|
||||
}
|
||||
|
||||
export default function Featured() {
|
||||
const query = graphql`
|
||||
query {
|
||||
allMarkdownRemark(
|
||||
filter: { frontmatter: { featured: { eq: true } } }
|
||||
sort: { fields: [fields___date], order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
const query = graphql`
|
||||
query {
|
||||
allMarkdownRemark(
|
||||
filter: { frontmatter: { featured: { eq: true } } }
|
||||
sort: { fields: [fields___date], order: DESC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
frontmatter {
|
||||
title
|
||||
image {
|
||||
childImageSharp {
|
||||
...ImageFluidThumb
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`
|
||||
|
||||
export default function Featured() {
|
||||
const data = useStaticQuery(query)
|
||||
return <FeaturedPure data={data} />
|
||||
}
|
||||
|
@ -1,19 +1,24 @@
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
.account {
|
||||
font-size: $font-size-mini;
|
||||
.accountWrap {
|
||||
font-size: $font-size-small;
|
||||
color: $brand-grey-light;
|
||||
max-width: 8rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: $spacer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.identicon {
|
||||
.blockies {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: $spacer / 8;
|
||||
margin-right: $spacer / 4;
|
||||
}
|
||||
|
||||
.balance {
|
||||
margin-left: $spacer;
|
||||
}
|
||||
|
14
src/components/molecules/Web3Donation/Account.test.tsx
Normal file
14
src/components/molecules/Web3Donation/Account.test.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import { render, waitForElement } from '@testing-library/react'
|
||||
|
||||
import Account from './Account'
|
||||
|
||||
describe('Account', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const { container } = render(<Account />)
|
||||
const lazyElement = await waitForElement(() =>
|
||||
container.querySelector('.balance')
|
||||
)
|
||||
expect(lazyElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,12 +1,27 @@
|
||||
import React from 'react'
|
||||
import Blockies from 'react-blockies'
|
||||
import { toDataUrl } from 'ethereum-blockies'
|
||||
import { formatEther } from '@ethersproject/units'
|
||||
import styles from './Account.module.scss'
|
||||
import useWeb3, { getBalance } from '../../../hooks/use-web3'
|
||||
|
||||
const Account = ({ account }: { account: string }) => (
|
||||
<div className={styles.account} title={account}>
|
||||
<Blockies seed={account} scale={2} size={8} className={styles.identicon} />
|
||||
{account}
|
||||
</div>
|
||||
)
|
||||
export default function Account() {
|
||||
const { library, account } = useWeb3()
|
||||
const ethBalance = account && getBalance(account, library)
|
||||
const blockies = account && toDataUrl(account)
|
||||
|
||||
export default Account
|
||||
const accountDisplay =
|
||||
account &&
|
||||
`${account.substring(0, 8)}...${account.substring(account.length - 4)}`
|
||||
const balanceDisplay =
|
||||
ethBalance && `Ξ${parseFloat(formatEther(ethBalance)).toPrecision(4)}`
|
||||
|
||||
return (
|
||||
<div className={styles.accountWrap} title={account}>
|
||||
<span className={styles.account}>
|
||||
<img className={styles.blockies} src={blockies} alt="Blockies" />
|
||||
{accountDisplay}
|
||||
</span>
|
||||
<span className={styles.balance}>{balanceDisplay}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
46
src/components/molecules/Web3Donation/Alert.tsx
Normal file
46
src/components/molecules/Web3Donation/Alert.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import styles from './Alert.module.scss'
|
||||
|
||||
export function getTransactionMessage(transactionHash?: string) {
|
||||
return {
|
||||
transaction: `<a href="https://etherscan.io/tx/${transactionHash}" target="_blank">See your transaction on etherscan.io.</a>`,
|
||||
waitingForUser: 'Waiting for your confirmation',
|
||||
waitingConfirmation: 'Waiting for network confirmation, hang on',
|
||||
success: 'Confirmed. You are awesome, thanks!'
|
||||
}
|
||||
}
|
||||
|
||||
const constructMessage = (
|
||||
transactionHash: string,
|
||||
message?: { text?: string }
|
||||
) =>
|
||||
transactionHash
|
||||
? message &&
|
||||
message.text +
|
||||
'<br /><br />' +
|
||||
getTransactionMessage(transactionHash).transaction
|
||||
: message && message.text
|
||||
|
||||
const classes = (status: string) =>
|
||||
status === 'success'
|
||||
? styles.success
|
||||
: status === 'error'
|
||||
? styles.error
|
||||
: styles.alert
|
||||
|
||||
export default function Alert({
|
||||
transactionHash,
|
||||
message
|
||||
}: {
|
||||
transactionHash: string
|
||||
message?: { text?: string; status?: string }
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classes(message.status)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${constructMessage(transactionHash, message)}`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import React from 'react'
|
||||
import styles from './Alerts.module.scss'
|
||||
|
||||
export const alertMessages = (
|
||||
networkName?: string,
|
||||
transactionHash?: string
|
||||
) => ({
|
||||
noAccount:
|
||||
'Web3 detected, but no account. Are you logged into your MetaMask account?',
|
||||
noCorrectNetwork: `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`,
|
||||
noWeb3:
|
||||
'No Web3 detected. Install <a href="https://metamask.io">MetaMask</a> or <a href="https://brave.com">Brave</a>.',
|
||||
transaction: `<a href="https://etherscan.io/tx/${transactionHash}" target="_blank">See your transaction on etherscan.io.</a>`,
|
||||
waitingForUser: 'Waiting for your confirmation',
|
||||
waitingConfirmation: 'Waiting for network confirmation, hang on',
|
||||
success: 'Confirmed. You are awesome, thanks!'
|
||||
})
|
||||
|
||||
interface AlertProps {
|
||||
transactionHash: string
|
||||
message?: { text?: string; status?: string }
|
||||
}
|
||||
|
||||
const constructMessage = (
|
||||
transactionHash: string,
|
||||
message?: { text?: string }
|
||||
) =>
|
||||
transactionHash
|
||||
? message &&
|
||||
message.text + '<br />' + alertMessages(null, transactionHash).transaction
|
||||
: message && message.text
|
||||
|
||||
const classes = (status: string) =>
|
||||
status === 'success'
|
||||
? styles.success
|
||||
: status === 'error'
|
||||
? styles.error
|
||||
: styles.alert
|
||||
|
||||
export default function Alerts({ transactionHash, message }: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
className={classes(message.status)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `${constructMessage(transactionHash, message)}`
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
@ -3,7 +3,10 @@
|
||||
.conversion {
|
||||
font-size: $font-size-mini;
|
||||
color: $brand-grey-light;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
margin-top: $spacer / 4;
|
||||
margin-left: $spacer * 1.4;
|
||||
animation: fadeIn 0.5s 0.8s ease-out backwards;
|
||||
|
||||
span {
|
||||
margin-left: $spacer / 2;
|
||||
|
12
src/components/molecules/Web3Donation/Conversion.test.tsx
Normal file
12
src/components/molecules/Web3Donation/Conversion.test.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { render, waitForElement } from '@testing-library/react'
|
||||
|
||||
import Conversion from './Conversion'
|
||||
|
||||
describe('Conversion', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const { getByText } = render(<Conversion amount={1} />)
|
||||
const lazyElement = await waitForElement(() => getByText(/= €/))
|
||||
expect(lazyElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,45 +1,45 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import { getFiat, Logger } from './utils'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import fetch from 'node-fetch'
|
||||
import styles from './Conversion.module.scss'
|
||||
|
||||
export default class Conversion extends PureComponent<
|
||||
{ amount: number },
|
||||
{ euro: string; dollar: string }
|
||||
> {
|
||||
state = {
|
||||
export async function getFiat(amount: number) {
|
||||
const url = 'https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=EUR'
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) console.error(response.statusText)
|
||||
const data = await response.json()
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const { price_usd, price_eur } = data[0]
|
||||
const dollar = (amount * price_usd).toFixed(2)
|
||||
const euro = (amount * price_eur).toFixed(2)
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
|
||||
return { dollar, euro }
|
||||
}
|
||||
|
||||
export default function Conversion({ amount }: { amount: number }) {
|
||||
const [conversion, setConversion] = useState({
|
||||
euro: '0.00',
|
||||
dollar: '0.00'
|
||||
}
|
||||
})
|
||||
const { dollar, euro } = conversion
|
||||
|
||||
componentDidMount() {
|
||||
this.getFiatResponse()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: any) {
|
||||
const { amount } = this.props
|
||||
|
||||
if (amount !== prevProps.amount) {
|
||||
this.getFiatResponse()
|
||||
}
|
||||
}
|
||||
|
||||
async getFiatResponse() {
|
||||
async function getFiatResponse() {
|
||||
try {
|
||||
const { dollar, euro } = await getFiat(this.props.amount)
|
||||
this.setState({ euro, dollar })
|
||||
const { dollar, euro } = await getFiat(amount)
|
||||
setConversion({ euro, dollar })
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dollar, euro } = this.state
|
||||
useEffect(() => {
|
||||
getFiatResponse()
|
||||
}, [amount])
|
||||
|
||||
return (
|
||||
<div className={styles.conversion}>
|
||||
<span>{dollar !== '0.00' && `= $ ${dollar}`}</span>
|
||||
<span>{euro !== '0.00' && `= € ${euro}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.conversion}>
|
||||
<span>{dollar !== '0.00' && `= $ ${dollar}`}</span>
|
||||
<span>{euro !== '0.00' && `= € ${euro}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -88,15 +88,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.infoline {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: $spacer / 4;
|
||||
animation: fadeIn 0.5s 0.8s ease-out backwards;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: message from './index.module.scss';
|
||||
}
|
||||
|
@ -1,41 +1,44 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import Input from '../../atoms/Input'
|
||||
import Account from './Account'
|
||||
import Conversion from './Conversion'
|
||||
import styles from './InputGroup.module.scss'
|
||||
|
||||
export default function InputGroup({
|
||||
amount,
|
||||
onAmountChange,
|
||||
sendTransaction,
|
||||
selectedAccount
|
||||
sendTransaction
|
||||
}: {
|
||||
amount: number
|
||||
onAmountChange(target: any): void
|
||||
sendTransaction(): void
|
||||
selectedAccount?: string | null
|
||||
sendTransaction(amount: number): void
|
||||
}) {
|
||||
const [amount, setAmount] = useState(0.03)
|
||||
|
||||
const onAmountChange = ({ target }: { target: any }) => {
|
||||
setAmount(target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.inputGroup}>
|
||||
<div className={styles.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={onAmountChange}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<div className={styles.currency}>
|
||||
<span>ETH</span>
|
||||
<div>
|
||||
<Account />
|
||||
<div className={styles.inputGroup}>
|
||||
<div className={styles.input}>
|
||||
<Input
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={onAmountChange}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<div className={styles.currency}>
|
||||
<span>ETH</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => sendTransaction(amount)}
|
||||
>
|
||||
Make it rain
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={sendTransaction}>
|
||||
Make it rain
|
||||
</button>
|
||||
<div className={styles.infoline}>
|
||||
<Conversion amount={amount} />
|
||||
{selectedAccount && <Account account={selectedAccount} />}
|
||||
</div>
|
||||
<Conversion amount={amount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +1,6 @@
|
||||
@import 'variables';
|
||||
|
||||
.web3 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: $spacer / 2;
|
||||
margin-bottom: $spacer;
|
||||
padding-bottom: $spacer;
|
||||
|
||||
small {
|
||||
color: darken($alert-info, 60%);
|
||||
margin-top: -($spacer / 2);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.web3Row {
|
||||
min-height: 77px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
26
src/components/molecules/Web3Donation/index.test.tsx
Normal file
26
src/components/molecules/Web3Donation/index.test.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { render, waitForElement, fireEvent } from '@testing-library/react'
|
||||
import { Web3ReactProvider } from '@web3-react/core'
|
||||
import { getLibrary } from '../../../hooks/use-web3'
|
||||
|
||||
import Web3Donation from '.'
|
||||
|
||||
describe('Web3Donation', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const { container, getByText } = render(
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3Donation address="xxx" />
|
||||
</Web3ReactProvider>
|
||||
)
|
||||
const lazyElement = await waitForElement(() =>
|
||||
container.querySelector('button')
|
||||
)
|
||||
expect(lazyElement).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(lazyElement)
|
||||
const message = await waitForElement(() =>
|
||||
getByText(/No Ethereum browser extension detected/)
|
||||
)
|
||||
expect(message).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,210 +1,70 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import Web3 from 'web3'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import useWeb3, { connectors, getErrorMessage } from '../../../hooks/use-web3'
|
||||
import InputGroup from './InputGroup'
|
||||
import Alerts, { alertMessages } from './Alerts'
|
||||
import Alert, { getTransactionMessage } from './Alert'
|
||||
import styles from './index.module.scss'
|
||||
import { getWeb3, getAccounts, getNetwork } from './utils'
|
||||
|
||||
const ONE_SECOND = 1000
|
||||
const ONE_MINUTE = ONE_SECOND * 60
|
||||
const correctNetwork = 1
|
||||
export default function Web3Donation({ address }: { address: string }) {
|
||||
const {
|
||||
connector,
|
||||
library,
|
||||
chainId,
|
||||
account,
|
||||
activate,
|
||||
active,
|
||||
error
|
||||
} = useWeb3()
|
||||
const [message, setMessage] = useState()
|
||||
|
||||
interface Web3DonationState {
|
||||
netId: number
|
||||
networkName: string
|
||||
accounts: string[]
|
||||
selectedAccount: string
|
||||
amount: number
|
||||
transactionHash: string
|
||||
receipt: string
|
||||
message: {
|
||||
status?: string
|
||||
text?: string
|
||||
}
|
||||
inTransaction: boolean
|
||||
}
|
||||
useEffect(() => {
|
||||
setMessage(undefined)
|
||||
|
||||
export default class Web3Donation extends PureComponent<
|
||||
{ address: string },
|
||||
Web3DonationState
|
||||
> {
|
||||
state = {
|
||||
netId: 0,
|
||||
networkName: '',
|
||||
accounts: [''],
|
||||
selectedAccount: '',
|
||||
amount: 0.01,
|
||||
transactionHash: '',
|
||||
receipt: '',
|
||||
message: {},
|
||||
inTransaction: false
|
||||
}
|
||||
|
||||
web3: Web3 = null
|
||||
interval: any = null
|
||||
networkInterval: any = null
|
||||
|
||||
componentDidMount() {
|
||||
this.initWeb3()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.resetAllTheThings()
|
||||
}
|
||||
|
||||
initWeb3 = async () => {
|
||||
this.setState({ message: { text: 'Checking' } })
|
||||
|
||||
try {
|
||||
this.web3 = await getWeb3()
|
||||
|
||||
this.web3
|
||||
? this.initAllTheTings()
|
||||
: this.setState({
|
||||
message: {
|
||||
status: 'error',
|
||||
text: alertMessages().noWeb3
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
message: { status: 'error', text: error }
|
||||
error &&
|
||||
setMessage({
|
||||
status: 'error',
|
||||
text: getErrorMessage(error, chainId)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [connector, account, library, chainId, active, error])
|
||||
|
||||
async initAllTheTings() {
|
||||
this.fetchAccounts()
|
||||
this.fetchNetwork()
|
||||
const [transactionHash, setTransactionHash] = useState(undefined)
|
||||
|
||||
this.initAccountsPoll()
|
||||
this.initNetworkPoll()
|
||||
}
|
||||
async function sendTransaction(amount: number) {
|
||||
const signer = library.getSigner()
|
||||
|
||||
resetAllTheThings() {
|
||||
clearInterval(this.interval)
|
||||
clearInterval(this.networkInterval)
|
||||
}
|
||||
|
||||
initAccountsPoll() {
|
||||
if (!this.interval) {
|
||||
this.interval = setInterval(this.fetchAccounts, ONE_SECOND * 10)
|
||||
}
|
||||
}
|
||||
|
||||
initNetworkPoll() {
|
||||
if (!this.networkInterval) {
|
||||
this.networkInterval = setInterval(this.fetchNetwork, ONE_MINUTE)
|
||||
}
|
||||
}
|
||||
|
||||
fetchNetwork = async () => {
|
||||
const { web3 } = this
|
||||
const { netId, networkName } = await getNetwork(web3)
|
||||
|
||||
if (netId === correctNetwork) {
|
||||
this.setState({ netId, networkName })
|
||||
} else {
|
||||
this.setState({
|
||||
message: {
|
||||
status: 'error',
|
||||
text: alertMessages(networkName).noCorrectNetwork
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fetchAccounts = async () => {
|
||||
const { web3 } = this
|
||||
const accounts = await getAccounts(web3)
|
||||
|
||||
if (accounts[0]) {
|
||||
this.setState({
|
||||
accounts,
|
||||
selectedAccount: accounts[0].toLowerCase()
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
message: {
|
||||
status: 'error',
|
||||
text: alertMessages().noAccount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sendTransaction = () => {
|
||||
const { web3 } = this
|
||||
|
||||
this.setState({
|
||||
inTransaction: true,
|
||||
message: { text: alertMessages().waitingForUser }
|
||||
setMessage({
|
||||
status: 'loading',
|
||||
text: getTransactionMessage().waitingForUser
|
||||
})
|
||||
|
||||
web3.eth
|
||||
.sendTransaction({
|
||||
from: this.state.selectedAccount,
|
||||
to: this.props.address,
|
||||
value: this.state.amount * 1e18 // ETH -> Wei
|
||||
})
|
||||
.once('transactionHash', transactionHash => {
|
||||
this.setState({
|
||||
transactionHash,
|
||||
message: { text: alertMessages().waitingConfirmation }
|
||||
})
|
||||
})
|
||||
.on('error', error =>
|
||||
this.setState({
|
||||
message: { status: 'error', text: error.message }
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
message: {
|
||||
status: 'success',
|
||||
text: alertMessages().success
|
||||
}
|
||||
})
|
||||
})
|
||||
const tx = await signer.sendTransaction({
|
||||
to: address,
|
||||
value: amount * 1e18 // ETH -> Wei
|
||||
})
|
||||
setTransactionHash(tx.hash)
|
||||
setMessage({
|
||||
status: 'loading',
|
||||
text: getTransactionMessage().waitingConfirmation
|
||||
})
|
||||
|
||||
await tx.wait()
|
||||
|
||||
setMessage({
|
||||
status: 'success',
|
||||
text: getTransactionMessage().success
|
||||
})
|
||||
}
|
||||
|
||||
onAmountChange = ({ target }: { target: any }) => {
|
||||
this.setState({ amount: target.value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedAccount,
|
||||
amount,
|
||||
transactionHash,
|
||||
message,
|
||||
inTransaction
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div className={styles.web3}>
|
||||
<header>
|
||||
<h4>Web3 Wallet</h4>
|
||||
<p>Send Ether with MetaMask or Brave.</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.web3Row}>
|
||||
{selectedAccount &&
|
||||
this.state.netId === correctNetwork &&
|
||||
!inTransaction ? (
|
||||
<InputGroup
|
||||
selectedAccount={selectedAccount}
|
||||
amount={amount}
|
||||
onAmountChange={this.onAmountChange}
|
||||
sendTransaction={this.sendTransaction}
|
||||
/>
|
||||
) : (
|
||||
message && (
|
||||
<Alerts message={message} transactionHash={transactionHash} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={styles.web3}>
|
||||
{!active && !message ? (
|
||||
<button className="link" onClick={() => activate(connectors.MetaMask)}>
|
||||
Activate Web3
|
||||
</button>
|
||||
) : library && account && !message ? (
|
||||
<InputGroup sendTransaction={sendTransaction} />
|
||||
) : (
|
||||
message && <Alert message={message} transactionHash={transactionHash} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
import Web3 from 'web3'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum: any
|
||||
web3: Web3
|
||||
}
|
||||
|
||||
interface Console {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static dispatch(verb: any, ...args: any) {
|
||||
console[verb](...args)
|
||||
}
|
||||
|
||||
static log(...args: any) {
|
||||
Logger.dispatch('log', ...args)
|
||||
}
|
||||
|
||||
static debug(...args: any) {
|
||||
Logger.dispatch('debug', ...args)
|
||||
}
|
||||
|
||||
static error(...args: any) {
|
||||
Logger.dispatch('error', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
export const getNetworkName = (netId: number) => {
|
||||
let networkName
|
||||
|
||||
switch (netId) {
|
||||
case 1:
|
||||
networkName = 'Main'
|
||||
break
|
||||
case 2:
|
||||
networkName = 'Morden'
|
||||
break
|
||||
case 3:
|
||||
networkName = 'Ropsten'
|
||||
break
|
||||
case 4:
|
||||
networkName = 'Rinkeby'
|
||||
break
|
||||
case 42:
|
||||
networkName = 'Kovan'
|
||||
break
|
||||
default:
|
||||
networkName = 'Private'
|
||||
}
|
||||
|
||||
return networkName
|
||||
}
|
||||
|
||||
export const getWeb3 = async () => {
|
||||
let web3
|
||||
|
||||
// Modern dapp browsers...
|
||||
if (window.ethereum) {
|
||||
web3 = new Web3(window.ethereum)
|
||||
|
||||
try {
|
||||
// Request account access
|
||||
await window.ethereum.enable()
|
||||
|
||||
return web3
|
||||
} catch (error) {
|
||||
// User denied account access...
|
||||
Logger.error(error)
|
||||
return error
|
||||
}
|
||||
}
|
||||
// Legacy dapp browsers...
|
||||
else if (window.web3) {
|
||||
web3 = new Web3(window.web3.currentProvider)
|
||||
|
||||
return web3
|
||||
}
|
||||
// Non-dapp browsers...
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export const getAccounts = async (web3: Web3) => {
|
||||
const ethAccounts = await web3.eth.getAccounts()
|
||||
|
||||
return ethAccounts
|
||||
}
|
||||
|
||||
export const getNetwork = async (web3: Web3) => {
|
||||
const netId = await web3.eth.net.getId()
|
||||
const networkName = getNetworkName(netId)
|
||||
|
||||
return { netId, networkName }
|
||||
}
|
||||
|
||||
export const getFiat = async (amount: number) => {
|
||||
const url = 'https://api.coinmarketcap.com/v1/ticker/ethereum/?convert=EUR'
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) Logger.error(response.statusText)
|
||||
const data = await response.json()
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const { price_usd, price_eur } = data[0]
|
||||
const dollar = (amount * price_usd).toFixed(2)
|
||||
const euro = (amount * price_eur).toFixed(2)
|
||||
/* eslint-enable @typescript-eslint/camelcase */
|
||||
|
||||
return { dollar, euro }
|
||||
}
|
75
src/hooks/use-web3/connectors.tsx
Normal file
75
src/hooks/use-web3/connectors.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { InjectedConnector } from '@web3-react/injected-connector'
|
||||
// import { NetworkConnector } from '@web3-react/network-connector'
|
||||
// import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
// import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
// import { LedgerConnector } from '@web3-react/ledger-connector'
|
||||
// import { TrezorConnector } from '@web3-react/trezor-connector'
|
||||
// import { FrameConnector } from '@web3-react/frame-connector'
|
||||
// import { AuthereumConnector } from '@web3-react/authereum-connector'
|
||||
// import { FortmaticConnector } from '@web3-react/fortmatic-connector'
|
||||
// import { PortisConnector } from '@web3-react/portis-connector'
|
||||
// import { SquarelinkConnector } from '@web3-react/squarelink-connector'
|
||||
// import { TorusConnector } from '@web3-react/torus-connector'
|
||||
|
||||
// const POLLING_INTERVAL = 8000
|
||||
// const RPC_URLS: { [chainId: number]: string } = {
|
||||
// 1: process.env.RPC_URL_1 as string,
|
||||
// 4: process.env.RPC_URL_4 as string
|
||||
// }
|
||||
|
||||
export const MetaMask = new InjectedConnector({
|
||||
supportedChainIds: [1]
|
||||
})
|
||||
|
||||
// export const network = new NetworkConnector({
|
||||
// urls: { 1: RPC_URLS[1], 4: RPC_URLS[4] },
|
||||
// defaultChainId: 1,
|
||||
// pollingInterval: POLLING_INTERVAL
|
||||
// })
|
||||
|
||||
// export const walletconnect = new WalletConnectConnector({
|
||||
// rpc: { 1: RPC_URLS[1] },
|
||||
// bridge: 'https://bridge.walletconnect.org',
|
||||
// qrcode: true,
|
||||
// pollingInterval: POLLING_INTERVAL
|
||||
// })
|
||||
|
||||
// export const walletlink = new WalletLinkConnector({
|
||||
// url: RPC_URLS[1],
|
||||
// appName: 'web3-react example'
|
||||
// })
|
||||
|
||||
// export const ledger = new LedgerConnector({
|
||||
// chainId: 1,
|
||||
// url: RPC_URLS[1],
|
||||
// pollingInterval: POLLING_INTERVAL
|
||||
// })
|
||||
|
||||
// export const trezor = new TrezorConnector({
|
||||
// chainId: 1,
|
||||
// url: RPC_URLS[1],
|
||||
// pollingInterval: POLLING_INTERVAL,
|
||||
// manifestEmail: 'dummy@abc.xyz',
|
||||
// manifestAppUrl: 'http://localhost:1234'
|
||||
// })
|
||||
|
||||
// export const frame = new FrameConnector({ supportedChainIds: [1] })
|
||||
|
||||
// export const authereum = new AuthereumConnector({ chainId: 42 })
|
||||
|
||||
// export const fortmatic = new FortmaticConnector({
|
||||
// apiKey: process.env.FORTMATIC_API_KEY as string,
|
||||
// chainId: 4
|
||||
// })
|
||||
|
||||
// export const portis = new PortisConnector({
|
||||
// dAppId: process.env.PORTIS_DAPP_ID as string,
|
||||
// networks: [1, 100]
|
||||
// })
|
||||
|
||||
// export const squarelink = new SquarelinkConnector({
|
||||
// clientId: process.env.SQUARELINK_CLIENT_ID as string,
|
||||
// networks: [1, 100]
|
||||
// })
|
||||
|
||||
// export const torus = new TorusConnector({ chainId: 1 })
|
99
src/hooks/use-web3/index.tsx
Normal file
99
src/hooks/use-web3/index.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
|
||||
import * as connectors from './connectors'
|
||||
import {
|
||||
getLibrary,
|
||||
getNetworkName,
|
||||
getErrorMessage,
|
||||
getBalance
|
||||
} from './utils'
|
||||
|
||||
export { connectors, getLibrary, getNetworkName, getErrorMessage, getBalance }
|
||||
|
||||
export function useEagerConnect() {
|
||||
const { MetaMask } = connectors
|
||||
const { activate, active } = useWeb3React()
|
||||
const [tried, setTried] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
MetaMask.isAuthorized().then(isAuthorized => {
|
||||
if (isAuthorized) {
|
||||
activate(MetaMask, undefined, true).catch(() => {
|
||||
setTried(true)
|
||||
})
|
||||
} else {
|
||||
setTried(true)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// if the connection worked, wait until we get confirmation of that to flip the flag
|
||||
useEffect(() => {
|
||||
if (!tried && active) {
|
||||
setTried(true)
|
||||
}
|
||||
}, [tried, active])
|
||||
|
||||
return tried
|
||||
}
|
||||
|
||||
export function useInactiveListener(suppress = false) {
|
||||
const { active, error, activate } = useWeb3React()
|
||||
const { MetaMask } = connectors
|
||||
|
||||
useEffect((): any => {
|
||||
const { ethereum } = window as any
|
||||
if (ethereum && !active && !error && !suppress) {
|
||||
const handleConnect = () => {
|
||||
console.log("Handling 'connect' event")
|
||||
activate(MetaMask)
|
||||
}
|
||||
const handleChainChanged = (chainId: string | number) => {
|
||||
console.log("Handling 'chainChanged' event with payload", chainId)
|
||||
activate(MetaMask)
|
||||
}
|
||||
const handleNetworkChanged = (networkId: string | number) => {
|
||||
console.log("Handling 'networkChanged' event with payload", networkId)
|
||||
activate(MetaMask)
|
||||
}
|
||||
const handleAccountsChanged = (accounts: string[]) => {
|
||||
console.log("Handling 'accountsChanged' event with payload", accounts)
|
||||
if (accounts.length > 0) {
|
||||
activate(MetaMask)
|
||||
}
|
||||
}
|
||||
|
||||
ethereum.on('connect', handleConnect)
|
||||
ethereum.on('chainChanged', handleChainChanged)
|
||||
ethereum.on('networkChanged', handleNetworkChanged)
|
||||
ethereum.on('accountsChanged', handleAccountsChanged)
|
||||
|
||||
return () => {
|
||||
ethereum.removeListener('networkChanged', handleNetworkChanged)
|
||||
ethereum.removeListener('accountsChanged', handleAccountsChanged)
|
||||
}
|
||||
}
|
||||
}, [active, error, suppress, activate])
|
||||
}
|
||||
|
||||
export default function useWeb3(): Web3ReactContextInterface {
|
||||
const context = useWeb3React()
|
||||
|
||||
// handle logic to recognize the connector currently being activated
|
||||
const [activatingConnector, setActivatingConnector] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
if (activatingConnector && activatingConnector === context.connector) {
|
||||
setActivatingConnector(undefined)
|
||||
}
|
||||
}, [activatingConnector, context.connector])
|
||||
|
||||
// handle logic to eagerly connect to the injected ethereum provider, if it exists and has granted access already
|
||||
const triedEager = useEagerConnect()
|
||||
|
||||
// handle logic to connect in reaction to certain events on the injected ethereum provider, if it exists
|
||||
useInactiveListener(!triedEager || !!activatingConnector)
|
||||
|
||||
return context
|
||||
}
|
83
src/hooks/use-web3/utils.tsx
Normal file
83
src/hooks/use-web3/utils.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { UnsupportedChainIdError } from '@web3-react/core'
|
||||
import {
|
||||
NoEthereumProviderError,
|
||||
UserRejectedRequestError
|
||||
} from '@web3-react/injected-connector'
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
|
||||
export function getLibrary(provider: any): Web3Provider {
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 10000
|
||||
return library
|
||||
}
|
||||
|
||||
export function getNetworkName(netId: number) {
|
||||
let networkName
|
||||
|
||||
switch (netId) {
|
||||
case 1:
|
||||
networkName = 'Main'
|
||||
break
|
||||
case 2:
|
||||
networkName = 'Morden'
|
||||
break
|
||||
case 3:
|
||||
networkName = 'Ropsten'
|
||||
break
|
||||
case 4:
|
||||
networkName = 'Rinkeby'
|
||||
break
|
||||
case 42:
|
||||
networkName = 'Kovan'
|
||||
break
|
||||
default:
|
||||
networkName = 'Private'
|
||||
}
|
||||
|
||||
return networkName
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: Error, chainId: number) {
|
||||
if (error instanceof NoEthereumProviderError) {
|
||||
return 'No Ethereum browser extension detected, install <a href="https://metamask.io">MetaMask</a> or <a href="https://brave.com">Brave</a>.'
|
||||
} else if (error instanceof UnsupportedChainIdError) {
|
||||
const networkName = getNetworkName(chainId)
|
||||
return `Please connect to <strong>Main</strong> network. You are on <strong>${networkName}</strong> right now.`
|
||||
} else if (error instanceof UserRejectedRequestError) {
|
||||
return 'Please authorize this website to access your Ethereum account.'
|
||||
} else {
|
||||
console.error(error)
|
||||
return 'An unknown error occurred. Check the console for more details.'
|
||||
}
|
||||
}
|
||||
|
||||
export function getBalance(account: string, library: any) {
|
||||
const [ethBalance, setEthBalance] = useState()
|
||||
|
||||
useEffect((): any => {
|
||||
if (library && account) {
|
||||
let stale = false
|
||||
|
||||
library
|
||||
.getBalance(account)
|
||||
.then((balance: any) => {
|
||||
if (!stale) {
|
||||
setEthBalance(balance)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setEthBalance(null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setEthBalance(undefined)
|
||||
}
|
||||
}
|
||||
}, [library, account])
|
||||
|
||||
return ethBalance
|
||||
}
|
@ -61,6 +61,20 @@
|
||||
font-size: $font-size-h2;
|
||||
}
|
||||
|
||||
.web3 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: $spacer / 2;
|
||||
margin-bottom: $spacer * 2;
|
||||
padding-bottom: $spacer;
|
||||
|
||||
small {
|
||||
color: darken($alert-info, 60%);
|
||||
margin-top: -($spacer / 2);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.coins {
|
||||
width: 100%;
|
||||
|
||||
|
@ -2,13 +2,15 @@ import React from 'react'
|
||||
import loadable from '@loadable/component'
|
||||
import shortid from 'shortid'
|
||||
import Helmet from 'react-helmet'
|
||||
import { Web3ReactProvider } from '@web3-react/core'
|
||||
import { Author } from '../@types/Site'
|
||||
import { useSiteMetadata } from '../hooks/use-site-metadata'
|
||||
import { getLibrary } from '../hooks/use-web3'
|
||||
import Qr from '../components/atoms/Qr'
|
||||
import Icon from '../components/atoms/Icon'
|
||||
import styles from './thanks.module.scss'
|
||||
|
||||
const Web3Donation = loadable(() =>
|
||||
const LazyWeb3Donation = loadable(() =>
|
||||
import('../components/molecules/Web3Donation')
|
||||
)
|
||||
|
||||
@ -46,10 +48,19 @@ export default function Thanks() {
|
||||
<h1 className={styles.title}>Say Thanks</h1>
|
||||
</header>
|
||||
|
||||
<Web3Donation
|
||||
fallback={<div className={styles.loading}>Loading...</div>}
|
||||
address={author.ether}
|
||||
/>
|
||||
<div className={styles.web3}>
|
||||
<header>
|
||||
<h4>Web3 Wallet</h4>
|
||||
<p>Send Ether with MetaMask or Brave.</p>
|
||||
</header>
|
||||
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<LazyWeb3Donation
|
||||
fallback={<div className={styles.loading}>Loading...</div>}
|
||||
address={author.ether}
|
||||
/>
|
||||
</Web3ReactProvider>
|
||||
</div>
|
||||
|
||||
<div className={styles.coins}>
|
||||
<header>
|
||||
|
Loading…
Reference in New Issue
Block a user