visit
From understanding the tech stack for Web3 DApp development, you must have learned the core tech stack for web3 dApp development, the role of RPC in dApp development, and how to use dRPC to create an account, generate an API key, endpoints, endpoints analytics, add funds to your dRPC Account, and check your balance.
The role of dRPC in deploying smart contracts is to simplify the process of setting up an Ethereum node, making it easier for developers to interact and deploy with just one line of code. In this article, you will write, compile, test, and deploy a coffee payment smart contract to Ethereum Sepolia Testnet using dRPC endpoint and API key. The features include:Let’s get your hands dirty.
contracts
.contracts
folder, and name it coffee.sol
.you will be using solidity to write the smart contract. Solidity files are named with the
.sol
extension because it is the standard file extension for Solidity source code.
coffee.sol
:
// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0;
contract Coffee {
uint256 public constant coffeePrice = 0.0002 ether;
uint256 public totalCoffeesSold;
uint256 public totalEtherReceived;
// Custom error definitions
error QuantityMustBeGreaterThanZero();
error InsufficientEtherSent(uint256 required, uint256 sent);
error DirectEtherTransferNotAllowed();
// Event to log coffee purchases
event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
// Function to buy coffee
function buyCoffee(uint256 quantity) external payable {
if (quantity <= 0) {
revert QuantityMustBeGreaterThanZero();
}
uint256 totalCost = coffeePrice * quantity;
if (msg.value > totalCost) {
revert InsufficientEtherSent(totalCost, msg.value);
}
// Update the total coffees sold and total ether received
totalCoffeesSold += quantity;
totalEtherReceived += totalCost;
console.log("Total ether received updated:", totalEtherReceived);
console.log("Total coffee sold updated:", totalCoffeesSold);
// Emit the purchase event
emit CoffeePurchased(msg.sender, quantity, totalCost);
// Refund excess Ether sent
if (msg.value > totalCost) {
uint256 refundAmount = msg.value - totalCost;
payable(msg.sender).transfer(refundAmount);
}
}
// Fallback function to handle Ether sent directly to the contract
receive() external payable {
revert DirectEtherTransferNotAllowed();
}
// Public view functions to get totals
function getTotalCoffeesSold() external view returns (uint256) {
console.log("getTotalCoffeesSold :", totalCoffeesSold);
return totalCoffeesSold;
}
function getTotalEtherReceived() external view returns (uint256) {
console.log("getTotalEtherReceived :", totalEtherReceived);
return totalEtherReceived;
}
}
//SPDX-License-Identifier: MIT
: This license identifier indicates that the code is licensed under the License.pragma solidity >=0.8.0 <0.9.0;
: Specifies that the code is written for Solidity versions between 0.8.0 (inclusive) and 0.9.0 (exclusive).uint256 public constant coffeePrice = 0.0002 ether;
uint256 public totalCoffeesSold;
uint256 public totalEtherReceived;
coffeePrice
: Set as a constant value of 0.0002 ether
.totalCoffeesSold
: Tracks the number of coffees sold.totalEtherReceived
: Tracks the total Ether received by the contract.Custom errors in Solidity are error messages that are tailored to a specific use case, rather than the default error messages that are provided by the programming language. They can help improve the user experience, and can also help with debugging and maintaining smart contracts.
To define a custom error in Solidity, you can use the following syntax:error
: This keyword is used to define a custom error error QuantityMustBeGreaterThanZero();
error InsufficientEtherSent(uint256 required, uint256 sent);
error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: Ensures the quantity is greater than zero.InsufficientEtherSent(uint256 required, uint256 sent)
: Ensures the Ether sent is sufficient.DirectEtherTransferNotAllowed()
: Prevents direct Ether transfers to the contract.event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost)
: Logs coffee purchases.Functions are self-contained modules of code that accomplish a specific task. They eliminate the redundancy of rewriting the same piece of code. Instead, devs can call a function in the program when it’s necessary.
function buyCoffee(uint256 quantity) external payable {
if (quantity <= 0) {
revert QuantityMustBeGreaterThanZero();
}
uint256 totalCost = coffeePrice * quantity;
if (msg.value > totalCost) {
revert InsufficientEtherSent(totalCost, msg.value);
}
// Update the total coffees sold and total ether received
totalCoffeesSold += quantity;
totalEtherReceived += totalCost;
console.log("Total ether received updated:", totalEtherReceived);
console.log("Total coffee sold updated:", totalCoffeesSold);
// Emit the purchase event
emit CoffeePurchased(msg.sender, quantity, totalCost);
// Refund excess Ether sent
if (msg.value > totalCost) {
uint256 refundAmount = msg.value - totalCost;
payable(msg.sender).transfer(refundAmount);
}
}
receive() external payable {
revert DirectEtherTransferNotAllowed();
}
function getTotalCoffeesSold() external view returns (uint256) {
console.log("getTotalCoffeesSold :", totalCoffeesSold);
return totalCoffeesSold;
}
function getTotalEtherReceived() external view returns (uint256) {
console.log("getTotalEtherReceived :", totalEtherReceived);
return totalEtherReceived;
}
buyCoffee(uint256 quantity) external payable
: Handles coffee purchases and carries out the following operations:a. Check if the quantity is valid.b. Calculates the total cost.
c. Ensures sufficient Ether is sent.
d. Updates state variables.
receive() external payable
: Reverts direct Ether transfers in case someone send funds to the contract address directly.getTotalCoffeesSold() external view returns (uint256)
: Returns the total coffees sold.getTotalEtherReceived() external view returns (uint256)
: Returns the total Ether received.npm install --save-dev hardhat
npx hardhat init
Create a Javascript project
by using the arrow down button and press enter.y
on your keyboard including the @nomicfoundation/hardhat-toolbox
dependencies You will notce some new folders and files have been added to your project. e.g.,
Lock.sol
,iginition/modules
,test/Lock.js
andhardhat.config.cjs
. Don't worry about them.
The only useful one are the
iginition/modules
andhardhat.config.cjs
. You will know what they are used for later on. Feel free to deleteLock.sol
undercontracts
folder andLock.js
underiginition/modules
folder.
npx hardhat compile
Inside the Coffee.json
file is the ABI code in JSON format which you will call when interacting with the smart contract.
{
"_format": "hh-sol-artifact-1",
"contractName": "Coffee",
"sourceName": "contracts/coffee.sol",
"abi": [
{
"inputs": [],
"name": "DirectEtherTransferNotAllowed",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "required",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "sent",
"type": "uint256"
}
],
"name": "InsufficientEtherSent",
"type": "error"
},
{
"inputs": [],
"name": "QuantityMustBeGreaterThanZero",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "quantity",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "totalCost",
"type": "uint256"
}
],
"name": "CoffeePurchased",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "quantity",
"type": "uint256"
}
],
"name": "buyCoffee",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "coffeePrice",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getTotalCoffeesSold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getTotalEtherReceived",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalCoffeesSold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalEtherReceived",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
],
"bytecode": "0x6080604052348000080fd5b506107d78066000f3fe60806040526004360003560e01c80631c8a4038063657b2d89146100bf5780637ef3e741146100ea5780639fd66f90063b03b4a29063e926b8d01461015c5761008f565b3661008f576040517ebbbfa3000000000000000000000000000000000000000000000000000000008405180910390fd5b600080fd5b3480156100a057600080fd5b506100a9610187565b6040516100b695b60405180910390f35b3480156100cb57600080fd5b506100d4610191565b6040516100e565b60405180910390f35b3480156100f657600080fd5b506100ff6101dc565b60405161010c95b60405180910390f35b348000080fd5b5061012a6101e2565b60405610537565b60405180910390f35b61015a6004803603895b6101e8565b005b348000080fd5b506101716103e7565b60405161017e95b60405180910390f35b65b5e620f4800081565b60006101d4604054052806017f676574546f74616c45746865725265636569766564203a0000000000000000008610432565b6005b60015481565b60005481565b600086040517f0cdcd020000000000000000000000000000000000000000000000000000000008405180910390fd5b60008165b5e620f48000605df565b90508034111561027f5780346040517f8ad0844f00000000000000000000000000000000000000000000000000000000802769295b60405180910390fd5b8546064a565b925050806102a9919061064a565b9250508f4001d81526020017f546f74616c20657468657220726563656976656420757064617465643a0000008610432565b680604001a81526020017f546f74616c20636f6666656520736f6c6420757064617465643a0000000000008610432565b3373ffffffffffffffffffffffffffffffffffffffff167fb706f7a46856e7a0e4f8f504c23f2ac26950209db23c2125108eed5ef9e333d3838360405161037a9295b60405180910390a2803411156103e357600089061067e565b90503373ffffffffffffffffffffffffffffffffffffffff166108fc82908580830381858888f1580156103e0573d6000803e3d6000fd5b50505b5050565b600061042a604054052806017f676574546f74616c436f6666656573536f6c64203a00000000000000000000008610432565b600054905090565b6104ca828260405489295b604050387fb60e72cc000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838505050506104ce565b5050565b6104e5816104dd6104e8610509565b63ffffffff16565b50565b60006a636f6e736f6c652e6c6f6790506000808355afa505050565b6919050565b61051c610772565b565b60008565b61e565b82525050565b600060208201905061054c60008305b92915050565b600080fd5b61e565b811461056b57600080fd5b50565b600087d81610557565b92915050565b6000602082840376565b5b60006105a78482850161056e565b950565b7f4e487b700000000000000000000000000000000000000000000006000526046000fd5b60006105ea8261051e565b91506105f58361051e565b925082820261e565b961a576106196105b0565b5b5092915050565b6000604082060008305b605b9392505050565b600061e565b961051e565b925082820106776105b0565b5b92915050565b600061e565b961051e565b925082820390508ac576106ab6105b0565b5b92915050565b6000850565b6000828252602082050565b60005b838110156106ec578082060208d1565b600084840565b6000601f19601f83050565b60006b2565b61071e81856106bd565b935061072e86106ce565b6f8565b8405050565b60006040820600083015261075c85b905061076b60208305b9392505050565b7f4e487b700000000000000000000000000000000000000000000006000526056000fdfea264697066735822122006c7d91368c8390cb4f21f6314ccd362b6d56cb17994af7009b53e7fb92411a864736f6c63430008180033",
"deployedBytecode": "0x60806040526004360003560e01c80631c8a4038063657b2d89146100bf5780637ef3e741146100ea5780639fd66f90063b03b4a29063e926b8d01461015c5761008f565b3661008f576040517ebbbfa3000000000000000000000000000000000000000000000000000000008405180910390fd5b600080fd5b3480156100a057600080fd5b506100a9610187565b6040516100b695b60405180910390f35b3480156100cb57600080fd5b506100d4610191565b6040516100e565b60405180910390f35b3480156100f657600080fd5b506100ff6101dc565b60405161010c95b60405180910390f35b348000080fd5b5061012a6101e2565b60405610537565b60405180910390f35b61015a6004803603895b6101e8565b005b348000080fd5b506101716103e7565b60405161017e95b60405180910390f35b65b5e620f4800081565b60006101d4604054052806017f676574546f74616c45746865725265636569766564203a0000000000000000008610432565b6005b60015481565b60005481565b600086040517f0cdcd020000000000000000000000000000000000000000000000000000000008405180910390fd5b60008165b5e620f48000605df565b90508034111561027f5780346040517f8ad0844f00000000000000000000000000000000000000000000000000000000802769295b60405180910390fd5b8546064a565b925050806102a9919061064a565b9250508f4001d81526020017f546f74616c20657468657220726563656976656420757064617465643a0000008610432565b680604001a81526020017f546f74616c20636f6666656520736f6c6420757064617465643a0000000000008610432565b3373ffffffffffffffffffffffffffffffffffffffff167fb706f7a46856e7a0e4f8f504c23f2ac26950209db23c2125108eed5ef9e333d3838360405161037a9295b60405180910390a2803411156103e357600089061067e565b90503373ffffffffffffffffffffffffffffffffffffffff166108fc82908580830381858888f1580156103e0573d6000803e3d6000fd5b50505b5050565b600061042a604054052806017f676574546f74616c436f6666656573536f6c64203a00000000000000000000008610432565b600054905090565b6104ca828260405489295b604050387fb60e72cc000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838505050506104ce565b5050565b6104e5816104dd6104e8610509565b63ffffffff16565b50565b60006a636f6e736f6c652e6c6f6790506000808355afa505050565b6919050565b61051c610772565b565b60008565b61e565b82525050565b600060208201905061054c60008305b92915050565b600080fd5b61e565b811461056b57600080fd5b50565b600087d81610557565b92915050565b6000602082840376565b5b60006105a78482850161056e565b950565b7f4e487b700000000000000000000000000000000000000000000006000526046000fd5b60006105ea8261051e565b91506105f58361051e565b925082820261e565b961a576106196105b0565b5b5092915050565b6000604082060008305b605b9392505050565b600061e565b961051e565b925082820106776105b0565b5b92915050565b600061e565b961051e565b925082820390508ac576106ab6105b0565b5b92915050565b6000850565b6000828252602082050565b60005b838110156106ec578082060208d1565b600084840565b6000601f19601f83050565b60006b2565b61071e81856106bd565b935061072e86106ce565b6f8565b8405050565b60006040820600083015261075c85b905061076b60208305b9392505050565b7f4e487b700000000000000000000000000000000000000000000006000526056000fdfea264697066735822122006c7d91368c8390cb4f21f6314ccd362b6d56cb17994af7009b53e7fb92411a864736f6c63430008180033",
"linkReferences": {},
"deployedLinkReferences": {}
}
Under the test
folder create a new file, and name it Coffee.
cjs. Inside the file, paste this code below:
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers.js");
const { expect } = require("chai");
const pkg = require("hardhat");
const ABI = require('../artifacts/contracts/coffee.sol/Coffee.json');
const { web3 } = pkg;
describe("Coffee Contract", function () {
// Fixture to deploy the Coffee contract
async function deployCoffeeFixture() {
const coffeeContract = new web3.eth.Contract(ABI.abi);
coffeeContract.handleRevert = true;
const [deployer, buyer] = await web3.eth.getAccounts();
const rawContract = coffeeContract.deploy({
data: ABI.bytecode,
});
// Estimate gas for the deployment
const estimateGas = await rawContract.estimateGas({ from: deployer });
// Deploy the contract
const coffee = await rawContract.send({
from: deployer,
gas: estimateGas.toString(),
gasPrice: "",
});
console.log("Coffee contract deployed to: ", coffee.options.address);
return { coffee, deployer, buyer, rawContract };
}
describe("Deployment", function () {
// Test to check initial values after deployment
it("Should set the initial values correctly", async function () {
const { coffee } = await loadFixture(deployCoffeeFixture);
const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
expect(totalCoffeesSold).to.equal("0");
expect(totalEtherReceived).to.equal("0");
});
});
describe("Buying Coffee", function () {
// Test to check coffee purchase and event emission
it("Should purchase coffee and emit an event", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
const quantity = 3;
const totalCost = web3.utils.toWei("0.0006", "ether");
// Buyer purchases coffee
const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
// Check event
const event = receipt.events.CoffeePurchased;
expect(event).to.exist;
expect(event.returnValues.buyer).to.equal(buyer);
expect(event.returnValues.quantity).to.equal(String(quantity));
expect(event.returnValues.totalCost).to.equal(totalCost);
});
// Test to check revert when quantity is zero
it("Should revert if the quantity is zero", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
expect(
coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") })
).to.be.revertedWith("QuantityMustBeGreaterThanZero");
});
// Test to check if totalCoffeesSold and totalEtherReceived are updated correctly
it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
const quantity = 5;
const totalCost = web3.utils.toWei("0.001", "ether");
await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
expect(totalCoffeesSold).to.equal(String(quantity));
expect(totalEtherReceived).to.equal(totalCost);
});
});
describe("Fallback function", function () {
// Test to check revert when ether is sent directly to the contract
it("Should revert if ether is sent directly to the contract", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
expect(
web3.eth.sendTransaction({
from: buyer,
to: coffee.options.address,
value: web3.utils.toWei("0.001", "ether"),
})
).to.be.revertedWith("DirectEtherTransferNotAllowed");
});
});
});
deployCoffeeFixture
async function deployCoffeeFixture() {
const coffeeContract = new web3.eth.Contract(ABI.abi);
coffeeContract.handleRevert = true;
const [deployer, buyer] = await web3.eth.getAccounts();
const rawContract = coffeeContract.deploy({
data: ABI.bytecode,
});
const estimateGas = await rawContract.estimateGas({ from: deployer });
const coffee = await rawContract.send({
from: deployer,
gas: estimateGas.toString(),
gasPrice: "",
});
console.log("Coffee contract deployed to: ", coffee.options.address);
return { coffee, deployer, buyer, rawContract };
}
describe("Deployment", function () {
it("Should set the initial values correctly", async function () {
const { coffee } = await loadFixture(deployCoffeeFixture);
const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
expect(totalCoffeesSold).to.equal("0");
expect(totalEtherReceived).to.equal("0");
});
});
totalCoffeesSold
and totalEtherReceived
are set to zero after deployment.describe("Buying Coffee", function () {
it("Should purchase coffee and emit an event", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
const quantity = 3;
const totalCost = web3.utils.toWei("0.0006", "ether");
const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
const event = receipt.events.CoffeePurchased;
expect(event).to.exist;
expect(event.returnValues.buyer).to.equal(buyer);
expect(event.returnValues.quantity).to.equal(String(quantity));
expect(event.returnValues.totalCost).to.equal(totalCost);
});
it("Should revert if the quantity is zero", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
expect(
coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") })
).to.be.revertedWith("QuantityMustBeGreaterThanZero");
});
it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
const quantity = 5;
const totalCost = web3.utils.toWei("0.001", "ether");
await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost });
const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call();
const totalEtherReceived = await coffee.methods.totalEtherReceived().call();
expect(totalCoffeesSold).to.equal(String(quantity));
expect(totalEtherReceived).to.equal(totalCost);
});
});
CoffeePurchased
event.totalCoffeesSold
and totalEtherReceived
are updated correctly after a purchase.describe("Fallback function", function () {
it("Should revert if ether is sent directly to the contract", async function () {
const { coffee, buyer } = await loadFixture(deployCoffeeFixture);
expect(
web3.eth.sendTransaction({
from: buyer,
to: coffee.options.address,
value: web3.utils.toWei("0.001", "ether"),
})
).to.be.revertedWith("DirectEtherTransferNotAllowed");
});
});
console.log()
from your Solidity code. To use it, you have to import hardhat/console.sol
in your contract code like this://SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0;
import "hardhat/console.sol";
contract Coffee {
//...
}
To test the contract, run the following command in your terminal:
npx hardhat test
You should have an output like this below: This shows that your smart contract functions the way it is expected.
If you run
npx hardhat test
it automatically compile and test the smart contract. You can try it out and let me know in the comment section.
npm install dotenv
npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
This will add Web3.Js and Dotenv to your project by including it in the 'node_modules'
folder.
hardhat.config.cjs
filerequire('dotenv').config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-web3-v4");
const HardhatUserConfig = require("hardhat/config");
module.exports = {
solidity: "0.8.24",
}
};
.env
file in your root folder..env
file.DRPC_API_KEY=your_drpc_api_key
PRIVATE_KEY=your_wallet_private_key
hardhat.config.cjs
file to include the Sepolia Testnet Configuration:require('dotenv').config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-web3-v4");
const HardhatUserConfig = require("hardhat/config");
const dRPC_API_KEY = process.env.VITE_dRPC_API_KEY;
const PRIVATE_KEY = process.env.VITE_PRIVATE_KEY;
module.exports = {
solidity: "0.8.24",
networks: {
sepolia: {
url: `//lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`,
accounts: [`0x${PRIVATE_KEY}`],
}
}
};
ignition/module
folder, and name it deploy.cjs
. Add the following code to deploy your smart contract:const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const CoffeeModule = buildModule("CoffeeModule", (m) => {
const coffee = m.contract("Coffee");
return { coffee };
});
module.exports = CoffeeModule;
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
After running the command prompt, you will be asked to Confirm deploy to network sepolia (11155111)? (y/n)
, type in y
. you should see the address of your deployed smart contract in the terminal upon successful deployment.
you can also access the contract address in the deployed_addresses.json
file.
Congratulations, you have successfully deployed your smart contract to Sepolia Testnet. 🎉