In this tutorial, I'm going to show you how to build an Ethereum Decentralized application or Dapp with React.js. If you are an absolute beginner, I suggest that you go over to the Ethereum docs for a proper introduction.
What we will be building
We'll be building a Dapp I call SendFunds, you can call it whatever you like. It will be a place where anybody can send ether(Ethereum's native currency) to any address and display the transaction on the front end.
We'll be writing and deploying a smart contract that will let people connect their wallets and interact with our smart contract. We will be deploying our Dapp to the Göerli testnet.
Installing dependencies
Node js
First we need to install the node package manager. To do this, head over to this website Node.js website.
You can see if you already have node installed by going to your terminal and typing:
node -v
Metamask wallet
We need metamask in order to interact with the ethereum blockchain. Visit this link to install metamask chrome plugin in your chrome browser.
Create-react-app
Next, we will need to install create-react-app which allows us to create and run a react application very easily without too much configuration. You can install it using the following commands:
npm install create-react-app
Sample project
Let's create our react app. Type in the following command in your terminal window.
npx create-react-app send-funds
cd send-funds
npm start
Your browser should open automatically. If it doesn't, head over to your browser and type http://localhost:3000
. You should see a webpage like this:
Hardhat
Hardhat is a tool that lets us compile our smart contract code quickly and test them locally. It creates a local Ethereum network for us that mimics the actual Ethereum mainnet. How cool is that!
Install Hardhat with the following commands:
npm install --save-dev hardhat
Make sure you are inside the send-funds
directory!
Sample project
Let's get a sample project running.
Run:
npx hardhat
Your terminal should look like this:
Choose the option to “Create a basic sample project”. Say yes to everything. In case you get a conflict error, delete the README.md
file in your root directory.
We need a few other dependencies. Let's install them.
npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers moment dotenv
Writing our smart contract code
Inside of your send-funds
folder, navigate to your contracts
folder and create a new file called SendFunds.sol
. You can go ahead and delete Greeter.sol
as we will not be needing it.
I will paste the code we will be needing below and explain what each line does.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "hardhat/console.sol";
contract SendFunds {
constructor() payable {
console.log("hello");
}
event NewTxn(address indexed to, uint256 amount, uint256 timestamp);
struct SentTransaction {
address reciever;
uint256 amount;
uint256 timestamp;
}
SentTransaction[] allTxn;
function sendFunds(address payable _to, uint256 amount) public payable {
require(amount <= address(this).balance, "not enough funds");
(bool success, ) = _to.call{value: amount}("");
require(success, "Unable to send Ether");
allTxn.push(SentTransaction(_to, amount, block.timestamp));
emit NewTxn(_to, amount, block.timestamp);
}
function getAllTxn() public view returns (SentTransaction[] memory) {
return allTxn;
}
}
//SPDX-License-Identifier: MIT
Every smart contract must begin with an // SPDX-License-Identifier
. If you do not do this, an error will occur.
pragma solidity ^0.8.13;
A version of solidity must be indicated next. We do this to tell the compiler to use version 0.8.13. To know more about license identifiers and compiler versions, check this out.
import "hardhat/console.sol";
Hardhat gives us a way to console.log
statements to our teminal.
contract SendFunds {
constructor() payable {
console.log("hello");
}
Smart contracts looks like a class
in other programming languages. The constructor
will run once when the contract is initialized for the first time and print what is in the console.log()
. We are making the constructor payable
because we want the smart contract to be able to receive ether.
event NewTxn(address indexed to, uint256 amount, uint256 timestamp);
Next is our event
. We need to display the transaction on our front end, we need an event
to be able to communicate to our front end that some state has changed!
SentTransaction[] allTxn;
struct
is used to group related data together. When sending a transaction, we need to store the receiver
, the amount
and the timestamp
.
function sendFunds(address payable _to, uint256 amount) public payable {
Next is our sendFunds function which takes in an address payable _to
meaning the address can accept payment. A uint256 amount
which takes in the amount to send to _to
and the function is payable
.
require(amount <= address(this).balance, "not enough funds");
The require
keyword returns a true or false. If the first half of it is true, it continues code execution. If false, it throws an error. Here, we are checking if the amount we want to send to the receiver is less than or equal to what the sender has in their wallet.
(bool success, ) = _to.call{value: amount}("");
Above is the magic line that actually sends ether to the receiver. Then we have another require
block to check if the transaction was a success.
allTxn.push(SentTransaction(_to, amount, block.timestamp));
emit NewTxn(_to, amount, block.timestamp);
Here, we are pushing _to
, amount
and block.timestamp
to our struct
instance and emitting it to the front end.
function getAllTxn() public view returns (SentTransaction[] memory) {
return allTxn;
}
For the final block of code, this function above returns all the transactions.
Testing out our smart contract
Before we begin, head over to your hardhat.config.js
file and change your version of solidity to 0.8.13
so it would match what you have in your SendFunds.sol
file.
In your scripts
folder, delete sample-script.js
and create two new files. run.js
is the first file to create. Here, we would play around with testing different aspects of our code and the next file to create is deploy.js
, here is the file we use to deploy our smart contract to your testnet.
The code below should be inside the run.js
file.
const hre = require("hardhat");
const main = async () => {
const sendFundsContractFactory = await hre.ethers.getContractFactory(
"SendFunds"
);
const sendFundsContract = await sendFundsContractFactory.deploy({
value: hre.ethers.utils.parseEther("4"),
});
await sendFundsContract.deployed();
console.log("contract address: ", sendFundsContract.address);
let contractBalance = await hre.ethers.provider.getBalance(
sendFundsContract.address
);
console.log(
"Contract balance:",
hre.ethers.utils.formatEther(contractBalance)
);
const [owner, randomPerson] = await hre.ethers.getSigners();
const sendFunds = await sendFundsContract
.connect(randomPerson)
.sendFunds(randomPerson.address, 2);
await sendFunds.wait();
const allTxn = await sendFundsContract.getAllTxn();
console.log(allTxn);
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
Let's go over this line by line.
const hre = require("hardhat");
We are requiring hardhat here because we will be needing it later.
const sendFundsContractFactory = await hre.ethers.getContractFactory(
"SendFunds"
);
This will compile our smart contract and generate the necessary files we need to work with our contract under the artifacts
folder.
const sendFundsContract = await sendFundsContractFactory.deploy({
value: hre.ethers.utils.parseEther("4")
});
Hardhat will create a local Ethereum network for us. Then, after the script completes it'll destroy that local network and we are giving the contract 4 ether.
await sendFundsContract.deployed();
Here, we are waiting for the contract to be deployed.
console.log("contract address: ", sendFundsContract.address);
let contractBalance = await hre.ethers.provider.getBalance(
sendFundsContract.address
);
console.log(
"Contract balance:",
hre.ethers.utils.formatEther(contractBalance)
);
Next, we are console.logging the contract's address and the contract's balance.
const [owner, randomPerson] = await hre.ethers.getSigners();
const sendFunds = await sendFundsContract
.connect(randomPerson)
.sendFunds(randomPerson.address, 2);
await sendFunds.wait();
What is going on here is we are getting a random user to send some ether to and we are calling the sendFunds
function passing in the random user's address and amount and waiting for the transaction to be completed.
const allTxn = await sendFundsContract.getAllTxn();
console.log(allTxn);
For the final bit of testing, we are calling the getAllTxn
function to get all of our transactions.
Run the following command in your terminal: npx hardhat run scripts/run.js
. Your terminal should be outputing the following:
Let's write our deploy script. It will be very similar to our run.js
file.
Input the following in your deploy.js
file.
const hre = require("hardhat");
const main = async () => {
const [deployer] = await hre.ethers.getSigners();
const accountBalance = await deployer.getBalance();
console.log("deploying contracts with account ", deployer.address);
console.log("account balance ", accountBalance.toString());
const sendFundsContractFactory = await hre.ethers.getContractFactory("SendFunds");
const sendFundsContract = await sendFundsContractFactory.deploy();
await sendFundsContract.deployed();
console.log("Funds contract address: ", sendFundsContract.address)
}
const runMain = async () => {
try {
await main();
process.exit(0)
} catch (error) {
console.log(error);
process.exit(1)
}
}
runMain();
Deploy to the Göerli testnet with Alchemy
We are going to be deploying to a testnet because deploying to the Ethereum Mainnet costs real money. I'll show you how to deploy to a testnet using Alchemy.
After logging in to Alchemy, on the top right corner, there is a create app button. Click on it
A pop up should appear next. Give your app a name, the chain should be Ethereum and network should be changed to Göerli. Finally click on create app button.
Next, click on your newly created project, it should take you to your project's dashboard. You will be needing the API url.
Next, create a .env
file in your root directory. We would be adding some things we don't want to public to get access to like your private key and API url. Don't forget to add your .env
file to your gitignore
file.
Head over to your hardhat.config.js
file. Input the following:
require("@nomiclabs/hardhat-waffle");
require('dotenv').config();
module.exports = {
solidity: "0.8.13",
networks: {
goerli: {
url: process.env.ALCHEMY_URL,
accounts: [process.env.WALLET_PRIVATE_KEY],
},
}
};
Let's go over this.
require('dotenv').config();
First, we are requiring dotenv
module.exports = {
solidity: "0.8.13",
networks: {
goerli: {
url: process.env.ALCHEMY_URL,
accounts: [process.env.WALLET_PRIVATE_KEY],
},
}
};
Next, we are filling in the url and accounts with our alchemy API url and our private key. To get your wallet's private key, head over here.
Please keep your private key safe to avoid loss of funds.
Before we deploy to the testnet, we need test Göerli. Head over to Göerli faucet. Login with Alchemy and paste in your wallet address. You should receive your test Göerli in a couple of seconds.
In your terminal, run the following commands to deploy your contract to the Göerli testnet: npx hardhat run scripts/deploy.js --network goerli
Your terminal should output the following:
Copy your contract's address. We are going to need it in the front end.
You've come a long way. Now let's connect our front end.
Setting up the front end
Let's start by deleting some unwanted code in your App.js
file under your src
folder. It should look like this:
import './App.css';
function App() {
return (
<div>
hello
</div>
);
}
export default App;
Next, we are going to be creating some new folders. Under your src
folder, create two new folders: components
and utils
.
Inside your components
folder, create two new files: Home.js
and Home.css
.
Inside of your Home.js
file. Input the following code:
import React, { useEffect, useState } from "react";
function Home() {
const [currentAccount, setCurrentAccount] = useState("");
const checkIfWalletIsConnected = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("you need to install metamask");
} else {
console.log("found one", ethereum);
}
/*
* Check if we're authorized to access the user's wallet
*/
const accounts = await ethereum.request({ method: "eth_accounts" });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("account ", account);
setCurrentAccount(account);
} else {
console.log("no authorized account found");
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
checkIfWalletIsConnected();
}, []);
return <div>Home</div>;
}
export default Home;
What is going on here is that we are basically checking to see if the special window.ethereum
object is injected in our browser. If not, you get a console.log
telling you to install Metamask. If the object is found, we use a special method called eth_accounts
to see if we're authorized to access any of the accounts in the user's wallet and since a user can have multiple accounts, we are taking the first one. Finally we are using the useEffect
hook to run the function immediately the page loads.
Connecting our wallet
Connecting our wallet is very easy to do. Your Home.js
file should look like the following:
import React, { useEffect, useState } from "react";
import "./Home.css";
function Home() {
const [currentAccount, setCurrentAccount] = useState("");
const checkIfWalletIsConnected = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("you need to install metamask");
} else {
console.log("found one", ethereum);
}
/*
* Check if we're authorized to access the user's wallet
*/
const accounts = await ethereum.request({ method: "eth_accounts" });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("account ", account);
setCurrentAccount(account);
} else {
console.log("no authorized account found");
}
} catch (error) {
console.log(error);
}
};
//connect wallet with button click
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("you need to install metamask");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
checkIfWalletIsConnected();
}, []);
//truncate wallet address
function truncate(input) {
return input.substring(0, 5) + "..." + input.substring(38);
}
return (
<div className="App">
{currentAccount.length === 0 ? (
<div>
<div className="nav">
<h1>SendFunds</h1>
</div>
<div className="content">
<div>
<p className="description">
Send <i className="fa-brands fa-ethereum"></i> to your friends
and family.
</p>
<button className="connect-btn" onClick={() => connectWallet()}>
Connect Wallet
</button>
</div>
</div>
</div>
) : (
<div>
<div className="nav flex">
<h1>SendFunds</h1>
<p className="wallet-address">{truncate(currentAccount)}</p>
</div>
<div className="content connected-wallet">
<p className="description">
Send <i className="fa-brands fa-ethereum"></i> to your friends and
family.
</p>
</div>
</div>
)}
</div>
);
}
export default Home;
Let's go over the connectWallet
and truncate
functions.
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
console.log("you need to install metamask");
return;
}
const accounts = await ethereum.request({
method: "eth_requestAccounts",
});
console.log("Connected", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};
Here, we are checking if window.ethereum
is present. If it is, we call eth_requestAccounts
to ask Metamask to give us access to the user's wallet. Then we are setting setCurrentAccount
to the first account.
function truncate(input) {
return input.substring(0, 5) + "..." + input.substring(38);
}
Since, wallet addresses are too long, we are truncating it.
Finally, we are doing some conditional rendering. If currentAccount.length === 0
, the user needs to connect their wallet, else display a welcome text.
The styling for the Home.css
page:
body{
background: rgb(100,0,123);
background: radial-gradient(circle, rgba(100,0,123,1) 0%, rgba(62,20,86,1) 100%);
color: #fff;
margin: 2px 40px;
font-family: 'Bellota', cursive;
}
.content {
text-align: center;
margin: 160px auto 40px;
}
.description {
font-size: 30px;
font-weight: bold;
}
.connect-btn {
color: white;
padding: 10px 30px;
font-size: 1.3em;
background: transparent;
border-radius: 50px;
border: 1px solid white;
margin: 10px auto 0;
cursor: pointer;
opacity: 0.7;
font-family: 'Bellota', cursive;
font-weight: bold;
}
.nav {
border-bottom: 1px solid #fff;
}
.nav h1 {
margin-bottom: 0;
text-align: left;
}
.flex {
display: flex;
align-items: center;
justify-content: space-between;
place-items: flex-end;
}
.nav p {
margin: 3px 0;
}
.connected-wallet {
margin: 70px auto 40px;
}
.wallet-address {
border: 1px solid #fff;
padding: 2px 15px;
border-radius: 50px;
}
I got my icon from font awesome and added the cdn to my index.html
file. For the font, I used Bellota from google fonts and also added the link to my index.html
file.
Import Home.js
in your App.js
file.
import './App.css';
import Home from './components/Home';
function App() {
return (
<div>
<Home />
</div>
);
}
export default App;
Run npm start
to check out your Dapp.
Your Home page should be looking like this:
Form implementation
Let's dive into our form creation. Under the utils
folder, create a new file called SendFunds.json
. This will house the artifacts gotten when you deployed your contract.
Under artifacts/contracts/SendFunds.sol
, you will find a SendFunds.json
file. Copy everything and paste in inside your utils/SendFunds.json
.
You also need to create two new files under your components
: Form.js
and Form.css
.
Let's create a custom form inside your Form.js
file:
import React, {useState} from 'react';
import './Form.css';
const Form = () => {
const [walletAddress, setWalletAddress] = useState('')
const [amount, setAmount] = useState('')
return (
<div className="form">
<form>
<p>
<input
type="text"
name=""
id=""
placeholder="Enter Wallet Address"
required
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
/>
</p>
<p>
<input
type="number"
name=""
id=""
placeholder="Enter Amount"
required
value={amount}
onChange={(e) => setAmount(e.target.value)}
step='any'
min='0'
/>
</p>
<button type="submit">
Send
</button>
</form>
</div>
);
};
export default Form;
The code above is pretty straight forward. Two input
fields. One a number
and the other a text
type. Then, I am saving the values in state.
Note: Don't forget to include your Form.js
file at the bottom of your Home.js
file.
Now, let's call our sendFunds
function from our smart contract.
import React, { useState } from "react";
import { ethers } from "ethers";
import abi from "../utils/SendFunds.json";
import { parseEther } from "ethers/lib/utils";
const Form = () => {
const [walletAddress, setWalletAddress] = useState("");
const [amount, setAmount] = useState("");
const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
const contractABI = abi.abi;
const sendFunds = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const sendFundsContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
const sendFundsTxn = await sendFundsContract.sendFunds(
walletAddress,
ethers.utils.parseEther(amount),
{ gasLimit: 300000, value: parseEther(amount) }
);
await sendFundsTxn.wait();
setWalletAddress('')
setAmount('')
} else {
console.log("ethereum object does not exist!");
}
} catch (error) {
console.log(error);
}
};
const handleSubmit = (e) => {
e.preventDefault();
sendFunds();
};
return (
<div className="form">
<form onSubmit={handleSubmit}>
<p>
<input
type="text"
name=""
id=""
placeholder="Enter Wallet Address"
required
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
/>
</p>
<p>
<input
type="number"
name=""
id=""
placeholder="Enter Amount"
required
value={amount}
onChange={(e) => setAmount(e.target.value)}
step="any"
min="0"
/>
</p>
<button type="submit">Send</button>
</form>
</div>
);
};
export default Form;
We have a lot going on here, so let's break it down.
import { ethers } from "ethers";
We are importing ethers
because we will need it in order to interact with our smart contract.
import abi from "../utils/SendFunds.json";
Next, we are importing our abi. You can read more about it here.
import { parseEther } from "ethers/lib/utils";
We use parseEther
when we want to convert a value from ETH to WEI which is the value we want to send to the contract when calling a payable method.
const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
The contract address gotten when we deployed our smart contract. Incase you didn't save yours, run npx hardhat run scripts/deploy.js --network goerli
.
const contractABI = abi.abi;
The abi gotten from our SendFunds.json
file.
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
A Provider is what we use to actually talk to Ethereum nodes. A signer is used to sign transactions and send the signed transactions to the Ethereum network. Read more abour signers here.
const sendFundsTxn = await sendFundsContract.sendFunds(
walletAddress,
ethers.utils.parseEther(amount),
{ gasLimit: 300000, value: parseEther(amount) }
);
await sendFundsTxn.wait();
We are calling the function we wrote in our smart contract and passing in the necessary arguments and waiting for the transaction to execute.
Next is the function to get all of our transactions:
import React, { useEffect, useState } from "react";
import { ethers } from "ethers";
import abi from "../utils/SendFunds.json";
import { parseEther } from "ethers/lib/utils";
import Transaction from "./Transactions";
import "./Form.css";
const Form = () => {
const [walletAddress, setWalletAddress] = useState("");
const [amount, setAmount] = useState("");
const [allTxns, setAllTxns] = useState([]);
const [isTxn, setIsTxn] = useState(false);
const contractAddress = "0x0FB172Db7Ab332f3ea5189C4A3659720124880Bc";
const contractABI = abi.abi;
const sendFunds = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const sendFundsContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
const sendFundsTxn = await sendFundsContract.sendFunds(
walletAddress,
ethers.utils.parseEther(amount),
{ gasLimit: 300000, value: parseEther(amount) }
);
await sendFundsTxn.wait();
} else {
console.log("ethereum object does not exist!");
}
} catch (error) {
console.log(error);
}
};
const handleSubmit = (e) => {
e.preventDefault();
sendFunds();
};
const getAllTransactions = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const sendFundsContract = new ethers.Contract(
contractAddress,
contractABI,
signer
);
let getAllTxn = await sendFundsContract.getAllTxn();
setIsTxn(true);
let txns = [];
getAllTxn.forEach((txn) => {
txns.push({
address: txn.reciever,
amount: txn.amount,
timestamp: new Date(txn.timestamp * 1000),
});
});
setAllTxns(txns);
} else {
console.log("ethereum object does not exist!");
}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getAllTransactions();
}, []);
useEffect(() => {
let sendFundsContract;
const onNewTransaction = (to, amount, timestamp) => {
console.log("New transaction", to, amount, timestamp);
setAllTxns(prevState => [
...prevState,
{
address: to,
amount: amount,
timestamp: new Date(timestamp * 1000)
},
]);
};
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
sendFundsContract = new ethers.Contract(contractAddress, contractABI, signer);
sendFundsContract.on("NewTxn", onNewTransaction);
}
return () => {
if (sendFundsContract) {
sendFundsContract.off("NewTxn", onNewTransaction);
}
};
}, []);
return (
<div className="form">
{/* don't forget to add the input fields, i removed them to make the code shorter */}
<div>
{isTxn === false ? (
<div></div>
) : (
<div>
<Transaction allTxns={allTxns} />
</div>
)}
</div>
</div>
);
};
export default Form;
We are calling the getAllTxn
function and pushing it into an array that we store with useState
and sending the array to the Transaction
component. You can go ahead and create a Transaction.js
and Transaction.css
files inside your components
folder.
I am also catching the event I created on the smart contract so I don't have to refresh the page whenever I send in a new transaction.
The styling I used for the form. Add it to your Form.css
file:
* {
font-family: 'Bellota', cursive;
}
button {
color: white;
padding: 10px 30px;
font-size: 1.3em;
background: transparent;
border-radius: 50px;
border: 1px solid white;
margin: 10px auto 0;
cursor: pointer;
opacity: 0.7;
font-weight: bold;
}
.form {
text-align: center;
margin: 60px auto 40px;
}
input {
border: 1px solid #fff;
padding: 8px 13px;
border-radius: 50px;
width: 30%;
margin-bottom: 20px;
font-weight: bold;
font-size: 18px;
}
For the final bit of this tutorial, let us display our transactions on the front end.
In your Transaction.js
file, input the following code:
import React from 'react';
import './Transaction.css'
import moment from 'moment'
import {ethers} from 'ethers'
const Transaction = ({allTxns}) => {
console.log(allTxns)
return (
<div className='transaction-container'>
<h2>All Transactions:</h2>
{allTxns.length === 0 ? <div>
</div>: <div className='grid-container'>
{allTxns.map((txn, index) => {
return (
<div key={index} className='transactions'>
<p>Reciever: {txn.address}</p>
<p>Amount: {ethers.utils.formatUnits(txn.amount.toString(), 'ether')} eth</p>
<p>Date: {moment(txn.timestamp.toString()).format('MM/DD/YYYY')}</p>
</div>
)
})}
</div>}
</div>
);
};
export default Transaction;
What is going on here is very clear. We are getting the allTxns
prop from the Form.js
file and we are displaying the data. Changing WEI to ETH using ethers.utils.formatUnits
and changing the txn.timestamp
to something more readable using moment
.
Styling for Transaction.css
file:
.transaction-container {
text-align: left;
margin-top: 20px;
}
.grid-container {
display: grid;
grid-template-columns: auto auto auto;
grid-gap: 10px;
}
.transactions{
background-color: #ffffff;
color: black;
padding: 0 10px;
border-radius: 10px;
width: 60%;
}
Run npm start
in your terminal. Send some transactions. Your web page should be looking like this:
Top comments (5)
Hi Johnson! This is an amazing article. Thank you for using Alchemy!
I work with the developer relations team at Alchemy. Would love to hear feedback from you. We'd also love to promote your article on our twitter.
I can be reached at aniket@alchemy.com
Thank you. This really means a lot to me. I have sent you a message on twitter. Of course, you can promote my article.
Of course. Thank you for your kind words ❤️
Great tutorial! Would you mind giving us access to your repository?
Thank you :)
github repo: github.com/derajohnson/send-funds-v2