Skip to main content

Command Palette

Search for a command to run...

Construire une DApp Multi-Paiements sur Morph avec Foundry

Published
‱9 min read
Construire une DApp Multi-Paiements sur Morph avec Foundry
D
Hello! I’m Daniel, a Web3 developer specializing in Solidity and smart contract development. My journey in the blockchain space is driven by a vision of a fully decentralized world, where technology empowers individuals and transforms industries. As a Web3 ambassador , I’m committed to fostering growth and innovation in this space, helping to shape a future that values transparency and security. Fluent in both English and French, I enjoy connecting with diverse communities and sharing my insights across languages. This is why you’ll find some of my articles in French, while others are in Swahili, as I believe knowledge should be accessible to all. I use my Hashnode blog to document my learning process, explore decentralized solutions, and share practical tutorials on Web3 development. Whether it's diving deep into Solidity, discussing the latest in blockchain, or exploring new tools, I’m passionate about contributing to a decentralized future and connecting with others who share this vision. Let’s build the future together!

Bonjour les dĂ©veloppeurs 👋
Ce tutoriel vous propose une approche pratique pour créer une application décentralisée (DApp) capable de gérer plusieurs types de paiements ou de devises, en utilisant Foundry, un framework populaire pour le développement de contrats intelligents.

PrĂ©requis 📚

  • Node JS (v16 ou plus)

  • NPM (v6 ou plus)

  • Solidity

  • Metamask

  • Ethers de testnet

  • JavaScript

Outils de dĂ©veloppement đŸ› ïž

  • Foundry
curl -L https://foundry.paradigm.xyz | bash

Commençons Ă  coder... đŸ‘šâ€đŸ’»

Étape 1 : CrĂ©er un nouveau rĂ©pertoire de projet

mkdir batchsender-dapp
  • Allez dans le rĂ©pertoire du projet nouvellement créé
cd batchsender-dapp

Étape 2 : Initialiser Foundry pour le dĂ©veloppement de contrats intelligents

✍ Foundry crĂ©e un nouveau dĂ©pĂŽt Git ; nous pouvons empĂȘcher cela en ajoutant une option Ă  la commande.

forge init --no-git

Structure du projet Foundry :

  • script

  • src

  • test
    ✍ Chaque dossier contient un fichier. Supprimez-les un par un pour avoir un espace de travail propre.

Étape 3 : CrĂ©er le code du contrat intelligent

  • Allez dans le rĂ©pertoire src

  • CrĂ©ez un nouveau fichier nommĂ© Batchsender.sol

  • Mettez Ă  jour Batchsender.sol avec ce 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]);
        }
    }
}

Étape 4 : Compiler le code du contrat intelligent

forge build
  • RĂ©sultat de la compilation :
[⠊] Compiling...
[â ą] Compiling 1 files with Solc 0.8.27
[⠆] Solc 0.8.27 finished in 139.58ms
Compiler run successful!

Étape 5 : Écrire le code de test en Solidity

  • Allez dans le rĂ©pertoire test

  • CrĂ©ez un nouveau fichier nommĂ© Batchsender.t.sol

  • Mettez Ă  jour Batchsender.t.sol avec ce 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]);
    }
}

✍ Vous pouvez utiliser vanity-eth.tk pour gĂ©nĂ©rer des adresses de portefeuilles pour vos tests.

Étape 6 : ExĂ©cuter les tests

forge test --match-path test/Batchsender.t.sol
  • Le rĂ©sultat du test devrait ressembler Ă  ceci :
[⠊] 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)

Étape 7 : DĂ©ployer le contrat intelligent sur le testnet Morph Holesky

  • CrĂ©ez un fichier .env dans le rĂ©pertoire du projet et ajoutez trois (3) variables d'environnement.
MORPH_RPC_URL="https://rpc-quicknode-holesky.morphl2.io"
DEV_PRIVATE_KEY="0x-insert-your-private-key"    // Prefix with 0x
CONTRACT_ADDRESS=""

✍ L'information CONTRACT_ADDRESS sera ajoutĂ©e aprĂšs le dĂ©ploiement.

  • Allez dans le rĂ©pertoire script et crĂ©ez un nouveau fichier nommĂ© Batchsender.s.sol

  • Ajoutez ce code pour le dĂ©ploiement du contrat :

// 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();
    }
}

Étape 8 : ExĂ©cuter le script de dĂ©ploiement

  • Chargez les informations d'environnement dans le terminal
source .env
  • Confirmez l'adresse du compte du dĂ©ployeur
forge script script/Batchsender.s.sol:BatchsenderScript
  • Cette exĂ©cution retournera l'adresse de portefeuille associĂ©e Ă  la clĂ© privĂ©e du dĂ©ployeur.
[⠊] 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
  • Simulez le contrat intelligent
forge script script/Batchsender.s.sol:BatchsenderScript --rpc-url $MORPH_RPC_URL
  • RĂ©sultat de la simulation rĂ©ussie :
[⠊] 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.

✍ Cette exĂ©cution crĂ©e automatiquement un nouveau rĂ©pertoire nommĂ© "broadcast" dans le projet pour faciliter le dĂ©ploiement du contrat intelligent.

  • ExĂ©cution finale pour dĂ©ployer le contrat
forge script script/Batchsender.s.sol:BatchsenderScript --rpc-url $MORPH_RPC_URL --broadcast --private-key $DEV_PRIVATE_KEY --legacy
  • Le rĂ©sultat du dĂ©ploiement devrait ressembler Ă  ceci :
[⠊] 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.

✍ Copiez l'adresse du contrat pour complĂ©ter la variable CONTRACT_ADDRESS.

Étape 9 : IntĂ©grer l'interface avec NextJs

npx create-next-app@latest

✍ Nommez votre interface "frontend" et sĂ©lectionnez la configuration par dĂ©faut pour le reste.

Étape 10 : Installer les plugins pour le dĂ©veloppement

  • Installez les trois (3) dĂ©pendances nĂ©cessaires pour le dĂ©veloppement cĂŽtĂ© client.
npm install ethers bootstrap react-csv-importer

Étape 11 : CrĂ©er le composant frontend

  • Ouvrez le rĂ©pertoire src

  • Le composant frontend se trouve dans app/page.js

  • Pour recommencer Ă  zĂ©ro, remplacez le code par celui-ci :

"use client"

export default function Home() {
  return ();
}
  • Mettez Ă  jour les imports :
"use client"

import { useState } from "react";
import { Importer, ImporterField } from "react-csv-importer";
import { ethers, Contract } from "ethers";

export default function Home() {
  return ();
}
  • Importez l'ABI du contrat :
// Import contract ABI
import {abi as contractABI} from '../../../out/Batchsender.sol/Batchsender.json';
  • Stockez l'adresse du contrat dans une variable :
// Contract address
const contractAddress = "0x8D69CCBf55ce078d9c9838a8861e0c827Dd4f2ff";
  • CrĂ©ez une variable objet pour l'explorateur blockchain :
// Blockchain Explorer object
const blockchainExplorerUrl = {
  2810: "https://rpc-quicknode-holesky.morphl2.io",
};
  • Mettez Ă  jour les variables d'Ă©tat :
// 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 ();
}
  • CrĂ©ez la fonction pour envoyer des paiements
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 ( );
}
  • Mettez Ă  jour la structure du composant JSX :
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>
      </>
    );
  }

Étape 12 : Ajouter des styles au composant

  • Allez dans le rĂ©pertoire src/app

  • CrĂ©ez un nouveau fichier nommĂ© style.css

  • Mettez Ă  jour style.css avec ce 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;
  }

Étape 13 : Configurer la structure de la mise en page

  • Ouvrez le fichier layout.js

  • Remplacez le code par ceci :

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>
  );
}

Étape 14 : Interagir avec la DApp

  • Pour commencer Ă  interagir avec la DApp, lancez le projet dans le navigateur
npm run dev

  • CrĂ©ez une feuille Excel avec les informations de vos transactions dans ce format :

  • Exportez ce fichier en .csv

  • Faites glisser et dĂ©posez votre fichier .csv dans la zone prĂ©vue ou cliquez pour en sĂ©lectionner un.

  • Cliquez sur "Importer" pour charger les informations dans la DApp pour traitement

  • Cliquez sur "Envoyer les transactions" pour effectuer les paiements en un clic

Conclusion

Dans ce guide, nous avons utilisé les outils les plus récents pour créer et déployer une application décentralisée sur la blockchain Morph, une solution Layer 2 qui combine les avantages des rollups zK et Optimistic pour des transactions rapides et sécurisées.
Bon codage... 🎉

S

"Thanks for the practical guide on setting up a multi-currency DApp with Foundry! This is a clear, step-by-step walkthrough with solid Solidity insights—perfect for both beginners and seasoned developers. Excited to dive in! đŸ‘šâ€đŸ’»âœš"

More from this blog

Untitled Publication

32 posts