Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to the course.
In our last lesson, you successfully built a component to transfer your ERC-20 tokens, which was a critical step in mastering "write" operations on the blockchain. You learned how to use a Signer to authorize transactions and how to manage the transaction lifecycle from submission to confirmation.
Today, we're going to build on that knowledge to create one of the most exciting parts of your dApp: a user interface for your token sale. This component will allow users to visit your site, connect their wallet, and purchase your token with Ether. This moves us from peer-to-peer transfers to direct interaction with the core business logic of your project's fundraising mechanism.
This lesson will introduce a key new concept: sending cryptocurrency (ETH) along with a function call. This is the mechanism that makes ICOs and crowdsales possible. By the end of this lesson, you will have a functional UI where users can participate in your token sale on the testnet.
1. The Goal: A Token Sale Interface
A token sale UI needs to be clear, informative, and trustworthy. It should show the user key information about the sale and make the purchasing process straightforward.

The main components of our UI will be:
- Sale Information: Displaying data from the contract, like the token price and the amount of tokens remaining.
- Purchase Form: An input for the user to specify how much ETH they want to spend.
- Dynamic Calculation: Real-time feedback showing how many tokens the user will receive for their ETH.
- Buy Button: A button to initiate the purchase transaction.
2. Architecting the Frontend: Reading Contract Data
First, let's create the basic structure of our new component, TokenSale.tsx. Just like with the TokenInfo component, we'll need to read data from our smart contract to populate the UI. We'll fetch information like the token exchange rate and the total funds raised so far.
The article below provides a fantastic, modern example of building a presale UI. We will use its core logic and component structure as our primary guide.
How to integrate ERC20 token presale smart contract with Frontend
Please read the introduction and view the code for the main PreSale component in the article 'How to integrate ERC20 token presale smart contract with Frontend'. This will give you a clear picture of the UI we're building and the logic involved.
First, review the introduction to see the final UI. Then, skim through the code for the PreSale component. Don't worry about understanding every line yet. Focus on identifying the state variables (payAmount, tokenAmount), the handlePayAmountChange function, and the handleBuy function. We will implement these pieces step-by-step.
As you saw in the article, a good first step is to fetch and display the current state of the sale. This involves connecting to your Crowdsale contract (which you deployed in Module 7) and calling its public view functions.
Let's start with a skeleton for TokenSale.tsx:
// src/TokenSale.tsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
// You'll need your Crowdsale contract's ABI and address
// import CrowdsaleABI from '../artifacts/contracts/Crowdsale.sol/Crowdsale.json';
// const crowdsaleAddress = 'YOUR_CROWDSALE_CONTRACT_ADDRESS';
const TokenSale = () => {
// State for contract data
const [rate, setRate] = useState<string>('');
const [weiRaised, setWeiRaised] = useState<string>('');
// State for the form
const [ethAmount, setEthAmount] = useState<string>('');
const [tokenAmount, setTokenAmount] = useState<string>('0');
// State for transaction status
const [isProcessing, setIsProcessing] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchSaleInfo = async () => {
if (window.ethereum) {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(crowdsaleAddress, CrowdsaleABI.abi, provider);
// Fetch data from the contract
const saleRate = await contract.rate();
const totalWeiRaised = await contract.weiRaised();
setRate(saleRate.toString());
setWeiRaised(ethers.formatEther(totalWeiRaised));
}
};
fetchSaleInfo();
}, []);
const handleEthAmountChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
// We'll implement this next
};
const handleBuy = async (e: React.FormEvent) => {
// And this is the main event!
};
return (
<div className='token-sale-card' style={{ border: '1px solid #ccc', borderRadius: '8px', padding: '20px', width: '400px', marginTop: '20px', backgroundColor: '#f9f9f9'}}>
<h3 style={{marginTop: 0}}>Participate in the Token Sale</h3>
<p><strong>Rate:</strong> 1 ETH = {rate} Tokens</p>
<p><strong>Total Raised:</strong> {weiRaised} ETH</p>
<form onSubmit={handleBuy}>
<div style={{ marginBottom: '10px' }}>
<label>ETH Amount:</label>
<input
type="text"
value={ethAmount}
onChange={handleEthAmountChange}
required
style={{ width: '100%', padding: '8px' }}
/>
</div>
<p>You will receive: {tokenAmount} Tokens</p>
<button type="submit" disabled={isProcessing} style={{ padding: '10px 20px' }}>
{isProcessing ? 'Processing...' : 'Buy Tokens'}
</button>
</form>
{txHash && <p>Success! Transaction Hash: {txHash}</p>}
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
</div>
);
};
export default TokenSale;
3. Dynamic Calculation: ETH to Tokens
A great user experience involves giving immediate feedback. As a user types in the amount of ETH they want to spend, we should instantly show them how many of our tokens they will receive.
We can achieve this by using our contract's rate. The logic is simple: tokenAmount = ethAmount * rate. This calculation happens entirely on the frontend. The handlePayAmountChange function from the dev.to article you reviewed does something similar, though it uses a contract function for estimation. For our simple rate model, frontend calculation is sufficient.
Let's implement the handleEthAmountChange function:
// Inside the TokenSale component
const handleEthAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const amount = e.target.value;
setEthAmount(amount);
if (amount && parseFloat(amount) > 0 && rate) {
// Use BigNumber for safe arithmetic with large numbers
const ethValue = ethers.parseEther(amount);
const tokenValue = ethValue * BigInt(rate); // rate is a string, needs to be BigInt
setTokenAmount(ethers.formatEther(tokenValue));
} else {
setTokenAmount('0');
}
};
This function updates the state as the user types, providing that real-time feedback.
4. The Main Event: Buying Tokens
Now we get to the core of the lesson: implementing the handleBuy function. This is where we will send a transaction to the buyTokens function on our Crowdsale contract. Since this function is payable, we need to tell ethers.js to send Ether along with the call.
In your backend work, you might send a JSON body with a POST request. Here, the concept is analogous: we're "attaching" a value (the ETH) to our function call.
The ethers.js v6 syntax for this is clean. When you call a contract function that should receive ETH, you pass an object as the last argument to the function call, containing a value property.
// Example syntax
const ethToSend = ethers.parseEther("1.0");
const tx = await contract.buyTokens(beneficiaryAddress, { value: ethToSend });
If your buyTokens function takes no arguments (e.g., msg.sender is the beneficiary), it looks even simpler:
const tx = await contract.buyTokens({ value: ethToSend });
The article below uses an older library (web3.js), but its buy function demonstrates this exact principle of sending a value.
Create and distribute your ERC20 token with OpenZeppelin
This article, 'Create and distribute your ERC20 token with OpenZeppelin', includes a simple React component for a crowdsale. Pay close attention to the buy function.
Focus on the buy(ev) function within the app.jsx code snippet. Notice the line: this.Crowdsale.methods.buyTokens(from).send({ value, from }). The key part is { value }, which attaches Ether to the transaction. We will implement the same logic using ethers.js.
Now, let's write our handleBuy function using the modern ethers.js syntax.
// Inside the TokenSale component
const handleBuy = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setTxHash(null);
setIsProcessing(true);
if (!window.ethereum) {
setError("MetaMask is not installed.");
setIsProcessing(false);
return;
}
try {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(crowdsaleAddress, CrowdsaleABI.abi, signer);
// The amount of ETH the user wants to spend, converted to wei
const value = ethers.parseEther(ethAmount);
// Call the 'buyTokens' function, sending ETH along with the transaction
// NOTE: Your buyTokens function might take an argument (like a beneficiary address)
// or it might not. Adjust accordingly. Here we assume it uses msg.sender.
// For a function like `buyTokens(address beneficiary)`, it would be:
// const txResponse = await contract.buyTokens(signer.address, { value });
const txResponse = await contract.buyTokens({ value });
console.log("Transaction sent! Hash:", txResponse.hash);
const txReceipt = await txResponse.wait();
console.log("Transaction confirmed! Receipt:", txReceipt);
setTxHash(txReceipt.hash);
// After a successful purchase, refetch the contract data to update the UI
const totalWeiRaised = await contract.weiRaised();
setWeiRaised(ethers.formatEther(totalWeiRaised));
} catch (err: any) {
console.error("Purchase failed:", err);
setError(err.reason || err.message || "An unknown error occurred.");
} finally {
setIsProcessing(false);
}
};
Test your understanding!
What would happen if you forgot to include the { value } object when calling the payable buyTokens function?
Show answer
The transaction would likely be sent to the blockchain, but it would fail and be reverted. The Crowdsale contract expects a payment (msg.value > 0). When you call the function without the { value } object, msg.value inside the contract execution is 0. A require(msg.value > 0) check within the smart contract would then trigger, causing the transaction to revert. You would lose the gas fees paid for the failed transaction.
5. Integration and Testing
You're all set! The final step is to import your new TokenSale.tsx component into your main App.tsx file and render it when a user's wallet is connected.
- Import the component in
App.tsx:
import TokenSale from './TokenSale'; - Add it to your render logic, perhaps below your
TransferTokencomponent:
Make sure the// In App.tsx's return statement {account && <TokenInfo ... />} {account && <TransferToken ... />} {account && <TokenSale />}crowdsaleAddressandCrowdsaleABIare correctly defined and accessible withinTokenSale.tsx.
Now, run your dApp (npm run dev), connect your MetaMask wallet to the Sepolia testnet, and you should see your token sale component.
Test the entire flow:
- Verify that the "Rate" and "Total Raised" data are correctly loaded from your deployed contract.
- Enter an amount of ETH (e.g., 0.01).
- Check if the "You will receive" text updates correctly.
- Click "Buy Tokens".
- Approve the transaction in the MetaMask pop-up. Notice that it shows the amount of ETH you are sending.
- Wait for the transaction to process. You should see the success message with the transaction hash.
- The "Total Raised" amount on your UI should update automatically to reflect your purchase.
Conclusion
Excellent work today! You have successfully built a core component of any cryptocurrency project: a user interface for a token sale. This is a significant milestone that brings together smart contract interaction, transaction management, and user interface design.
Key Takeaways:
- Sending ETH with Transactions: The most critical concept was attaching Ether to a function call using the
{ value: ... }object inethers.js. - Reading Contract State: You populated your UI by fetching data (like
rateandweiRaised) from your smart contract using aProvider. - Frontend Calculations: You provided a responsive user experience by calculating the expected token amount in real-time as the user typed.
- Full Transaction Lifecycle: You handled the entire process from user input to submitting the transaction with a
Signer, waiting for confirmation, and updating the UI with the new state.
Preview of the Next Lesson:
With the token sale UI complete, we'll move on to another key feature of modern DeFi applications: staking. In the next lesson, you will develop a UI that allows users to stake their newly purchased tokens and claim rewards, further enhancing the utility and appeal of your dApp.