Hello Devs 👋
This tutorial provides a hands-on approach, guiding developers through building a decentralized application (DApp) capable of handling multiple payment types or currencies, while using Foundry—a popular smart contract development framework.
Prerequisites 📚
- Node JS (v16 or later)
- NPM (v6 or later)
- Solidity
- Metamask
- Testnet ethers
- JavaScript
Dev Tools 🛠️
- Foundry
curl -L https://foundry.paradigm.xyz | bash
Let's start hacking... 👨💻
Step 1: Create a new project directory
mkdir batchsender-dapp
- Navigate into the new project directory
cd batchsender-dapp
Step 2: Initialize Foundry framework for smart contract development
✍️ Foundry creates a new git repository; we may prevent this by flagging the command.
forge init --no-git
Foundry file tree:
- script
- src
- test
✍️ Each of these folders contains a file. Delete them one by one to create a clean slate.
Step 3: Create the smart contract code
- Navigate to the
src
directory - Create a new file named
Batchsender.sol
- Update
Batchsender.sol
with the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
contract Batchsender {
// Custom Error for mismatched recipients and amounts
error MismatchedArrayLengths(
uint256 recipientsLength,
uint256 amountsLength
);
// Custom error for insufficient ethers
error NotEnoughEth();
// Custom Error for failed transfer
error TransferFailed(address recipients, uint256 amounts);
// Event to log each successful transfer
event TokenSent(address indexed recipients, uint256 amounts);
// Payable function for Ether transfer
function sendToken(
address payable[] calldata recipients,
uint[] calldata amounts
) external payable {
// Check if recipient length and amount length are the same
if (recipients.length != amounts.length) {
revert MismatchedArrayLengths(recipients.length, amounts.length);
}
// Calculate the total amount to be sent
uint totalAmount = 0;
for (uint i = 0; i < amounts.length; i++) {
totalAmount += amounts[i];
}
// Ensure the sent amount is equal or greater than the total amount
if (msg.value < totalAmount) {
revert NotEnoughEth();
}
// Loop through recipients array to match recipients to amounts
for (uint i = 0; i < recipients.length; i++) {
(bool sendSuccess, ) = recipients[i].call{value: amounts[i]}("");
if (!sendSuccess) {
revert TransferFailed(recipients[i], amounts[i]);
}
// Emit event for each successful transfer
emit TokenSent(recipients[i], amounts[i]);
}
}
}
Step 4: Compile the smart contract code
forge build
- Compilation result:
[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.27
[⠆] Solc 0.8.27 finished in 139.58ms
Compiler run successful!
Step 5: Write solidity code for unit tests
- Navigate to the
test
directory - Create a new file named
Batchsender.t.sol
- Update
Batchsender.t.sol
with the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {Test, console} from "forge-std/Test.sol";
import {Batchsender} from "../src/Batchsender.sol";
contract BatchsenderTest is Test {
Batchsender public batchsender;
function setUp() public {
// Deploy the contract before each test
batchsender = new Batchsender();
}
function test_sendToken() public {
address payable[] memory recipients = new address payable[](2);
uint[] memory amounts = new uint[](2);
recipients[0] = payable(0x6c5fa1b41990f4ee402984Bcc8Bf6F4CB769fE74);
recipients[1] = payable(0x55829bC84132E1449b62607B1c7bbC012f0326Ac);
amounts[0] = 100; //wei
amounts[1] = 200; //wei
batchsender.sendToken{value: 300}(recipients, amounts);
assertEq(address(recipients[0]).balance, amounts[0]);
assertEq(address(recipients[1]).balance, amounts[1]);
}
}
✍️ You can visit vanity-eth.tk to generate new wallet addresses for your test.
Step 6: Running the test
forge test --match-path test/Batchsender.t.sol
- Test result should look like this:
[⠊] Compiling...
[⠘] Compiling 24 files with Solc 0.8.27
[⠃] Solc 0.8.27 finished in 733.97ms
Compiler run successful!
Ran 1 test for test/Batchsender.t.sol:BatchsenderTest
[PASS] test_sendToken() (gas: 90741)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.33ms (1.46ms CPU time)
Ran 1 test suite in 151.38ms (8.33ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Step 7: Deploying smart contract to Morph Holesky testnet
- Create a
.env
file in the project directory and add three (3) environment variables.
MORPH_RPC_URL="https://rpc-quicknode-holesky.morphl2.io"
DEV_PRIVATE_KEY="0x-insert-your-private-key" // Prefix with 0x
CONTRACT_ADDRESS=""
✍️ The
CONTRACT_ADDRESS
information will be added after deployment.
Navigate to the
script
directory and create a new file namedBatchsender.s.sol
Add the following code for smart contract deployment
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {Script, console} from "forge-std/Script.sol";
import {Batchsender} from "../src/Batchsender.sol";
contract BatchsenderScript is Script {
Batchsender public batchsender;
function setUp() public {}
function run() public {
// Save Private key as variable for reusability
uint privateKey = vm.envUint("DEV_PRIVATE_KEY");
// start deployment...with Private Key
vm.startBroadcast(privateKey);
// Log Account to the console
address account = vm.addr(privateKey);
console.log("Deployer Account address: ", account);
batchsender = new Batchsender();
vm.stopBroadcast();
}
}
Step 8: Running the deployment script
- Load the environment information into the CLI
source .env
- Confirm the deployer's account address
forge script script/Batchsender.s.sol:BatchsenderScript
- This execution will return the wallet address associated with the deployer's private key.
[⠊] Compiling...
[⠑] Compiling 2 files with Solc 0.8.27
[⠘] Solc 0.8.27 finished in 650.26ms
Compiler run successful!
Script ran successfully.
Gas used: 259115
== Logs ==
Deployer Account address: 0x4E1856E40D53e2893803f1da919F5daB713B215c
- Simulate the smart contract
forge script script/Batchsender.s.sol:BatchsenderScript --rpc-url $MORPH_RPC_URL
- Successful simulation result:
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Logs ==
Deployer Account address: 0x4E1856E40D53e2893803f1da919F5daB713B215c
## Setting up 1 EVM.
==========================
Chain 2810
Estimated gas price: 0.15063575 gwei
Estimated total gas used for script: 340061
Estimated amount required: 0.00005122534378075 ETH
==========================
SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more.
✍️ This execution automatically creates a new directory named
broadcast
in the project for seamless smart contract deployment.
- Final execution for smart contract deployment
forge script script/Batchsender.s.sol:BatchsenderScript --rpc-url $MORPH_RPC_URL --broadcast --private-key $DEV_PRIVATE_KEY --legacy
- The deployment result should look like this:
[⠊] Compiling...
No files changed, compilation skipped
Script ran successfully.
== Logs ==
Deployer Account address: 0x4E1856E40D53e2893803f1da919F5daB713B215c
## Setting up 1 EVM.
==========================
Chain 2810
Estimated gas price: 0.14963575 gwei
Estimated total gas used for script: 340061
Estimated amount required: 0.00005088528278075 ETH
==========================
##### 2810
✅ [Success]Hash: 0xe65413c0b8ff406c18c33b77c385bc3d46bcd9305030dd49aa2277d3c90bf69a
Contract Address: 0x8D69CCBf55ce078d9c9838a8861e0c827Dd4f2ff
Block: 11252084
Paid: 0.0000391521939875 ETH (261650 gas * 0.14963575 gwei)
✅ Sequence #1 on 2810 | Total Paid: 0.0000391521939875 ETH (261650 gas * avg 0.14963575 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
✍️ Copy the contract address to populate the
CONTRACT_ADDRESS
environment variable.
Step 9: Integrating the frontend with NextJs
npx create-next-app@latest
✍️ Name your app frontend and select the default configuration for the rest of the prompt.
Step 10: Install plugins for development
Install the three (3) dependencies needed for the development of the client-side of the application.
npm install ethers bootstrap react-csv-importer
Step 11: Creating the frontend component
- Open the
src
directory - The frontend component is structured in the
app/page.js
file - To start afresh, replace the default code with the following:
"use client"
export default function Home() {
return ();
}
- Update the imports:
"use client"
import { useState } from "react";
import { Importer, ImporterField } from "react-csv-importer";
import { ethers, Contract } from "ethers";
export default function Home() {
return ();
}
- Import the contract ABI:
// Import contract ABI
import {abi as contractABI} from '../../../out/Batchsender.sol/Batchsender.json';
- Store the contract address in a variable:
// Contract address
const contractAddress = "0x8D69CCBf55ce078d9c9838a8861e0c827Dd4f2ff";
- Create an object variable for the blockchain explorer:
// Blockchain Explorer object
const blockchainExplorerUrl = {
2810: "https://rpc-quicknode-holesky.morphl2.io",
};
- Update the state variables:
// Some code...
export default function Home() {
// State variables
const [payments, setPayments] = useState(undefined);
const [sending, setSending] = useState(false);
const [blockchainExplorer, setBlockchainExplorer] = useState();
const [error, setError] = useState(false);
const [transaction, setTransaction] = useState(false);
return ();
}
- Create the function for sending payments
export default function Home() {
// Some state variable codes...
// Function for sending payments
const sendPayments = async () => {
// Connect to Metamask
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const chainIdBigInt = (await provider.getNetwork()).chainId;
const chainId = Number(chainIdBigInt); // convert to interger;
setBlockchainExplorer(blockchainExplorerUrl[chainId.toString()]);
// Show feedback to users
setSending(true);
// Format arguements for smart contract => Convert CSV row to column
const { recipient, amount, total } = payments.reduce(
(acc, val) => {
acc.recipient.push(val.recipient);
acc.amount.push(val.amount);
acc.total += Number(val.amount);
return acc;
},
{
recipient: [],
amount: [],
total: 0,
}
);
// Send transaction
const batchsenderContract = new Contract(
contractAddress,
contractABI,
signer
);
try {
const transaction = await batchsenderContract.sendToken(
recipient,
amount,
{
value: total,
}
);
const transactionReceipt = await transaction.wait();
setTransaction(transactionReceipt.hash);
} catch (error) {
console.log(error);
setError(true);
}
};
return ( );
}
- Update the JSX component structure:
export default function Home() {
// Some state variable and function codes...
return (
<>
<div className="container-fluid mt-5 d-flex justify-content-center">
<div id="content" className="row">
<div id="content-inner" className="col">
<div className="text-center">
<h1 id="title" className="fw-bold">
BATCHSENDER
</h1>
<p id="sub-title" className="mt-4 fw-bold">
Send multiple payments <br />{" "}
<span>in just one transaction</span>
</p>
</div>
<Importer
dataHandler={(rows) => setPayments(rows)}
defaultNoHeader={false} // optional, keeps "data has headers" checkbox off by default
restartable={false} // optional, lets user choose to upload another file when import is complete
>
<ImporterField name="recipient" label="recipient" />
<ImporterField name="amount" label="amount" />
<ImporterField name="asset" label="asset" />
</Importer>
<div className="text-center">
<button
className="btn btn-primary mt-5"
onClick={sendPayments}
disabled={sending || typeof payments === "undefined"}
>
Send transactions
</button>
</div>
{sending && (
<div className="alert alert-info mt-4 mb-0">
Please wait while your transaction is being processed...
</div>
)}
{transaction && (
<div className="alert alert-success mt-4 mb-0">
Congrats! Transaction processed successfully. <br />
<a
href={`${blockchainExplorer}/${transaction}`}
target="_blank"
>{`${transaction.substr(0, 20)}...`}</a>
</div>
)}
{error && (
<div className="alert alert-danger mt-4 mb-0">
Oops...there was an error. Please try again later!
</div>
)}
</div>
</div>
</div>
</>
);
}
Step 12: Adding styles to the component
- Navigate to the
src/app
directory - Create a new file named
style.css
- Update
style.css
with the following code:
#content {
width: 700px;
}
#content-inner {
background-color: rgba(240, 240, 240);
border-radius: 10px;
padding: 1em;
}
#title {
font-family: "Permanent Marker", cursive;
font-size: 2em;
font-style: normal;
font-weight: 400;
}
#sub-title {
font-size: 1.5em;
}
#sub-title span {
border-bottom: 5px solid #085ed6;
}
#CSVImporter_Importer {
margin-top: 3em;
}
Step 13: Configure the layout structure
- Open the
layout.js
file - Replace the code with the following:
import "bootstrap/dist/css/bootstrap.min.css";
import "react-csv-importer/dist/index.css";
import "./style.css";
export const metadata = {
title: "Batchsender",
description: "Make multiple crypto payments in one click",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Permanent+Marker&display=swap"
rel="stylesheet"
/>
</head>
<body>{children}</body>
</html>
);
}
Step 14: Interacting with the DApp
- To start interacting with the DApp, launch project in the browser
npm run dev
- Create an Excel spreadsheet containing the information of your transaction in this format:
Recipient | Amount | Asset |
---|---|---|
Wallet Addr1 | 50 | ETH |
Wallet Addr2 | 100 | ETH |
Wallet Addr3 | 200 | ETH |
- Export this file to a Comma Separated Values
.csv
file - Drag and drop your
.csv
file on the marked area or click to select one from your files
- Select
Choose columns
to load-up the information - Click the
Import
button to import the information to the DApp for processing
- Select
Send transactions
to make payments in one click
Conclusion
In this complete guide, we have successfully used the most recent blockchain development tools to build and deploy a dynamic decentralized application on the Morph blockchain, which is a Layer 2 solution that combines the benefits of both zK and Optimistic rollups for secure and rapid transactions.
Happy hacking... 🎉
Top comments (0)