-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathDapp.js
411 lines (352 loc) · 14.1 KB
/
Dapp.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
import React from 'react'
// We'll use ethers to interact with the Ethereum network and our contract
import { ethers } from 'ethers'
// We import the contract's artifacts and address here, as we are going to be
// using them with ethers
import simpleTokenArtifact from '../contracts/SimpleToken.json'
import contractAddress from '../contracts/contract-address.json'
import deployerAccount from '../contracts/deployer.json'
// All the logic of this dapp is contained in the Dapp component.
// These other components are just presentational ones: they don't have any
// logic. They just render HTML.
import { NoWalletDetected } from './NoWalletDetected'
import { ConnectWallet } from './ConnectWallet'
import { Loading } from './Loading'
import { Transfer } from './Transfer'
import { TransactionErrorMessage } from './TransactionErrorMessage'
import { WaitingForTransactionMessage } from './WaitingForTransactionMessage'
import { NoTokensMessage } from './NoTokensMessage'
// This is the Hardhat Network id, you might change it in the hardhat.config.js
// Here's a list of network ids https://docs.metamask.io/guide/ethereum-provider.html#properties
// to use when deploying to other networks.
const HARDHAT_NETWORK_ID = '11155111'
// This is an error code that indicates that the user canceled a transaction
const ERROR_CODE_TX_REJECTED_BY_USER = 4001
// get the SimpleToken contract address
let simpleTokenContractAddress = contractAddress.contractAddress
//get the contract deployer
const contractDeployer = deployerAccount.deployer
// This component is in charge of doing these things:
// 1. It connects to the user's wallet
// 2. Initializes ethers and the Token contract
// 3. Polls the user balance to keep it updated.
// 4. Transfers tokens by sending transactions
// 5. Renders the whole application
//
// Note that (3) and (4) are specific of this sample application, but they show
// you how to keep your Dapp and contract's state in sync, and how to send a
// transaction.
export class Dapp extends React.Component {
constructor(props) {
super(props)
// We store multiple things in Dapp's state.
// You don't need to follow this pattern, but it's an useful example.
this.initialState = {
// The info of the token (i.e. It's Name and symbol)
tokenData: undefined,
// The user's address and balance
selectedAddress: undefined,
balance: undefined,
// The ID about transactions being sent, and any possible error with them
txBeingSent: undefined,
deployBegin: undefined,
transactionError: undefined,
networkError: undefined,
decimals: undefined,
}
this.state = this.initialState
}
render () {
// Ethereum wallets inject the window.ethereum object. If it hasn't been
// injected, we instruct the user to install MetaMask.
if (window.ethereum === undefined) {
return <NoWalletDetected />
}
// The next thing we need to do, is to ask the user to connect their wallet.
// When the wallet gets connected, we are going to save the users's address
// in the component's state. So, if it hasn't been saved yet, we have
// to show the ConnectWallet component.
//
// Note that we pass it a callback that is going to be called when the user
// clicks a button. This callback just calls the _connectWallet method.
if (!this.state.selectedAddress) {
return (
<ConnectWallet
connectWallet={() => this._connectWallet()}
networkError={this.state.networkError}
dismiss={() => this._dismissNetworkError()}
/>
)
}
// If the token data or the user's balance hasn't loaded yet, we show
// a loading component.
if (!this.state.tokenData || !this.state.balance) {
return <Loading />
}
// If everything is loaded, we render the application.
return (
<div className="container p-4">
<div className="row">
<div className="col-12">
<h1>
{this.state.tokenData.name} ({this.state.tokenData.symbol})
</h1>
<p>
Welcome <b>{this.state.selectedAddress}</b>, you have{' '}
<b>
{/* show human read balance (deployed contract with precise 1 in /scripts/deploy.js) */}
{ethers.formatUnits(this.state.balance, this.state.decimals)} {this.state.tokenData.symbol}
</b>
.
</p>
</div>
</div>
<hr />
<div className="row">
<div className="col-12">
{/*
Sending a transaction isn't an immidiate action. You have to wait
for it to be mined.
If we are waiting for one, we show a message here.
*/}
{this.state.txBeingSent && (
<WaitingForTransactionMessage txHash={this.state.txBeingSent} />
)}
{/*
Sending a transaction can fail in multiple ways.
If that happened, we show a message here.
*/}
{this.state.transactionError && (
<TransactionErrorMessage
message={this._getRpcErrorMessage(this.state.transactionError)}
dismiss={() => this._dismissTransactionError()}
/>
)}
</div>
</div>
<div className="row">
<div className="col-12">
{/*
If the user has no tokens, we don't show the Transfer form
*/}
{this.state.balance === 0 && (
<NoTokensMessage deployContract={() => this._deployContract()} />
)}
{this.state.deployBegin && <Loading />}
{/*
This component displays a form that the user can use to send a
transaction and transfer some tokens.
The component doesn't have logic, it just calls the transferTokens
callback.
*/}
{this.state.balance > 0 && (
<Transfer
transferTokens={(to, amount) =>
// convert to contract precise amount
this._transferTokens(to, ethers.parseUnits(amount, this.state.decimals))
}
/>
)}
</div>
</div>
</div>
)
}
componentWillUnmount () {
// We poll the user's balance, so we have to stop doing that when Dapp
// gets unmounted
this._stopPollingData()
}
async _connectWallet () {
// This method is run when the user clicks the Connect. It connects the
// dapp to the user's wallet, and initializes it.
// To connect to the user's wallet, we have to run this method.
// It returns a promise that will resolve to the user's address.
const [selectedAddress] = await window.ethereum.request({ method: 'eth_requestAccounts' });
// Once we have the address, we can initialize the application.
// First we check the network
if (!this._checkNetwork()) {
return
}
this._initialize(selectedAddress)
// We reinitialize it whenever the user changes their account.
window.ethereum.on('accountsChanged', ([newAddress]) => {
this._stopPollingData()
// `accountsChanged` event can be triggered with an undefined newAddress.
// This happens when the user removes the Dapp from the "Connected
// list of sites allowed access to your addresses" (Metamask > Settings > Connections)
// To avoid errors, we reset the dapp state
if (newAddress === undefined) {
return this._resetState()
}
this._initialize(newAddress)
})
// We reset the dapp state if the network is changed
window.ethereum.on('chainChanged', ([_chainId]) => {
this._stopPollingData()
this._resetState()
})
}
_initialize (userAddress) {
// This method initializes the dapp
// We first store the user's address in the component's state
this.setState({
selectedAddress: userAddress,
})
// Then, we initialize ethers, fetch the token's data, and start polling
// for the user's balance.
// Fetching the token data and the user's balance are specific to this
// sample project, but you can reuse the same initialization pattern.
this._intializeEthers()
this._getTokenData()
this._startPollingData()
}
async _intializeEthers () {
// We first initialize ethers by creating a provider using window.ethereum
this._provider = new ethers.BrowserProvider(window.ethereum)
const signer = await this._provider.getSigner();
// When, we initialize the contract using that provider and the token's
// artifact. You can do this same thing with your contracts.
console.log('signer', signer, simpleTokenContractAddress,)
this._simpleToken = new ethers.Contract(
simpleTokenContractAddress,
simpleTokenArtifact.abi,
signer
)
console.log('simpleToken', this._simpleToken)
this.setState({ decimals: await this._simpleToken.decimals() })
}
// The next to methods are needed to start and stop polling data. While
// the data being polled here is specific to this example, you can use this
// pattern to read any data from your contracts.
//
// Note that if you don't need it to update in near real time, you probably
// don't need to poll it. If that's the case, you can just fetch it when you
// initialize the app, as we do with the token data.
_startPollingData () {
this._pollDataInterval = setInterval(() => this._updateBalance(), 1000)
// We run it once immediately so we don't have to wait for it
this._updateBalance()
}
_stopPollingData () {
clearInterval(this._pollDataInterval)
this._pollDataInterval = undefined
}
// The next two methods just read from the contract and store the results
// in the component state.
async _getTokenData () {
const name = await this._simpleToken?.name()
const symbol = await this._simpleToken?.symbol()
console.log('name', name, symbol)
this.setState({ tokenData: { name, symbol } })
}
async _updateBalance () {
const balance = await this._simpleToken?.balanceOf(
this.state.selectedAddress
)
console.log('balance', balance)
// ethers.BigNumber.isBigNumber(balance)
this.setState({ balance })
}
// This method sends an ethereum transaction to transfer tokens.
// While this action is specific to this application, it illustrates how to
// send a transaction.
async _transferTokens (to, amount) {
// Sending a transaction is a complex operation:
// - The user can reject it
// - It can fail before reaching the ethereum network (i.e. if the user
// doesn't have ETH for paying for the tx's gas)
// - It has to be mined, so it isn't immediately confirmed.
// Note that some testing networks, like Hardhat Network, do mine
// transactions immediately, but your dapp should be prepared for
// other networks.
// - It can fail once mined.
//
// This method handles all of those things, so keep reading to learn how to
// do it.
try {
// If a transaction fails, we save that error in the component's state.
// We only save one such error, so before sending a second transaction, we
// clear it.
this._dismissTransactionError()
// We send the transaction, and save its hash in the Dapp's state. This
// way we can indicate that we are waiting for it to be mined.
const tx = await this._simpleToken.transfer(to, amount)
this.setState({ txBeingSent: tx.hash })
// We use .wait() to wait for the transaction to be mined. This method
// returns the transaction's receipt.
const receipt = await tx.wait()
// The receipt, contains a status flag, which is 0 to indicate an error.
if (receipt.status === 0) {
// We can't know the exact error that make the transaction fail once it
// was mined, so we throw this generic one.
throw new Error('Transaction failed')
}
// If we got here, the transaction was successful, so you may want to
// update your state. Here, we update the user's balance.
await this._updateBalance()
} catch (error) {
// We check the error code to see if this error was produced because the
// user rejected a tx. If that's the case, we do nothing.
if (error.code === ERROR_CODE_TX_REJECTED_BY_USER) {
return
}
// Other errors are logged and stored in the Dapp's state. This is used to
// show them to the user, and for debugging.
console.error(error)
this.setState({ transactionError: error })
} finally {
// If we leave the try/catch, we aren't sending a tx anymore, so we clear
// this part of the state.
this.setState({ txBeingSent: undefined })
}
}
// This method just clears part of the state.
_dismissTransactionError () {
this.setState({ transactionError: undefined })
}
// This method just clears part of the state.
_dismissNetworkError () {
this.setState({ networkError: undefined })
}
// This is an utility method that turns an RPC error into a human readable
// message.
_getRpcErrorMessage (error) {
if (error.data) {
return error.data.message
}
return error.message
}
// This method resets the state
_resetState () {
this.setState(this.initialState)
}
// This method checks if Metamask selected network is Localhost:8545
_checkNetwork () {
if (window.ethereum.networkVersion === HARDHAT_NETWORK_ID) {
return true
}
this.setState({
networkError: 'Please connect Metamask to Sepolia',
})
return false
}
async _deployContract () {
this.setState({ deployBegin: true })
let simpleTokenContractFactory = new ethers.ContractFactory(
simpleTokenArtifact.abi,
simpleTokenArtifact.bytecode,
this._provider.getSigner(0)
)
let simpleTokenContract = await simpleTokenContractFactory.deploy(
'hello',
'Dapp',
1,
100000000
)
await simpleTokenContract.deployed()
simpleTokenContractAddress = simpleTokenContract.address
await this._intializeEthers()
this.setState({ deployBegin: undefined })
}
}