Prevent Bots
This guide shows you how to write and deploy a smart contract in Move that uses reCAPTCHA to verify users are human (and not bots) before they interact with the contract. CAPTCHA is a method of bot mitigation that requires you to pass a challenge test to prove that you are human. CAPTCHA tests are effective in preventing bots from performing tasks, but the tests can become annoying or frustrating for legitimate users if they are too difficult or frequent. reCAPTCHA is a form of CAPTCHA testing.
This guide assumes you have installed Sui and understand Sui fundamentals.
Move smart contract
As with all Sui dApps, a Move package on chain powers the logic of the reCAPTCHA module. The following instruction walks you through creating and publishing the module.
reCAPTCHA module
Before you get started, you must initialize a Move package. Open a terminal or console in the directory you want to store the example and run the following command to create an empty package with the name recaptcha
:
sui move new recaptcha
With that done, it's time to jump into some code. Create a new file in the sources
directory with the name recaptcha.move
and populate the file with the following code:
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
module recaptcha::recaptcha {
// Import the vector module for manipulating vectors.
use std::vector;
// Import the clock module for getting the current time.
use sui::clock::Clock;
// Import the dynamic_field module for adding custom fields to objects.
use sui::dynamic_field as df;
// Import the ed25519 module for verifying signatures.
use sui::ed25519::ed25519_verify;
// Import the event module for emitting events.
use sui::event::emit;
// Import the math module for performing mathematical operations.
use sui::math;
// Import the object module for creating and manipulating objects.
use sui::object::{Self, UID};
// Import the transfer module for sharing and transferring objects.
use sui::transfer;
// Import the tx_context module for accessing transaction information.
use sui::tx_context::{sender, TxContext};
/// Error code for transactions that violate the cooldown period.
const EVerificationExpired: u64 = 0;
/// Error code for invalid signatures.
const EInvalidSignature: u64 = 1;
/// Error code for senders that are not yet verified.
const ENotYetVerified: u64 = 2;
// Define a constant for the duration of the time window in milliseconds
const TIME_WINDOW: u64 = 60_000;
}
There are few details to take note of in this code:
- The fourth line declares the module name as
recaptcha
within the packagerecaptcha
. - The next eight lines use the use keyword to import types and functions from other modules, such as
sui::clock::Clock
,sui::dynamic_field as df
,sui::ed25519::ed25519_verify
,sui::event::emit,sui::math
,sui::object::{Self, UID}
,sui::transfer
, andsui::tx_context::{sender, TxContext}
. These modules are needed for the implementation of the reCAPTCHA verification and the interaction logic. - The following three lines declare the error codes, namely:
EVerificationExpired
,EInvalidSignature
, andENotYetVerified
, that are used to check the validity of the reCAPTCHA test result and the eligibility of the user. The error codes are also used in the unit tests to verify the correctness of the program. - The last line declares the constant
TIME_WINDOW
, which specifies the duration of the time window in milliseconds. The time window is the period of time that the user is eligible to interact with the smart contract after passing the reCAPTCHA test.
Next, add some more code to this module:
struct Interaction has copy, drop {
sender: address, // The address of the sender
timestamp_ms: u64, // The timestamp in milliseconds
}
// Define a struct for the registry object that has a key field
struct Registry has key {
id: UID, // The unique identifier of the registry object
window: u64, // The length of the time window in milliseconds
}
// Define a function for initializing the registry
fun init(ctx: &mut TxContext) {
// Share the registry object with other participants
transfer::share_object(
Registry {
id: object::new(ctx), // Create a new object with a unique id
window: TIME_WINDOW, // Set the time window to the constant value
}
);
}
- The
Interaction
struct is used to define the data that is emitted as an event when a user successfully interacts with the smart contract. TheInteraction
event has two fields: the sender's address and the timestamp in milliseconds. The sender's address is the account that initiated the interaction, and the timestamp is the current time when the event is triggered. - The
Registry
struct stores the mapping of the user’s address to the expiration time of the eligibility to interact with the smart contract. It also has awindow
field that specifies the length of the time window in milliseconds. The time window is the period of time that the user is eligible to interact with the smart contract after passing the reCAPTCHA test. - The
init
function creates a shared object for theRegistry
. The function is called when the smart contract is deployed to the blockchain. The function creates a new registry object with a unique id and sets the time window to the constant value.
So far, you've set up the data structures within the module. Now, create a function that verifies the message
/// @param registry: The registry object.
/// @param signature: 32-byte signature that is a point on the Ed25519 elliptic curve.
/// @param public_key: 32-byte signature that is a point on the Ed25519 elliptic curve.
/// @param msg: The message that we test the signature against.
public fun verify(
registry: &mut Registry,
signature: vector<u8>,
public_key: vector<u8>,
msg: vector<u8>,
ctx: &mut TxContext
) {
let verified = ed25519_verify(&signature, &public_key, &msg);
assert!(verified, EInvalidSignature);
if (!df::exists_with_type<address, u64>(®istry.id, sender(ctx))) {
df::add<address, u64>(
&mut registry.id,
sender(ctx),
msg_to_ts(&msg)
);
} else {
let timestamp_ms = df::borrow_mut<address, u64>(&mut registry.id, sender(ctx));
*timestamp_ms = msg_to_ts(&msg);
}
}
/// Function to get the timestamp_ms from the message, which is a vector of bytes, and transform it to a u64.
public fun msg_to_ts(
message: &vector<u8>
): u64 {
let vec_length = vector::length(message);
let (value, i) = (0u64, 0u8);
while (i < 13) {
let element = (*vector::borrow(message, vec_length - (i as u64) - 1) - 48 as u64); // '0' = 48
value = value + element * math::pow(10, i); // 10^i
i = i + 1;
};
value
}
The verify
function is a public function that allows anyone to register themselves as non-bot using the function call. The function takes five parameters:
registry
: The registry object that stores the mapping of the user's address to the expiration time of the eligibility.signature
: The 32-byte signature that is a point on the Ed25519 elliptic curve. The signature is generated by the oracle using its private key and the message that contains the user's address and the current timestamp.public_key
: The 32-byte public key that is a point on the Ed25519 elliptic curve. The public key is the oracle's public key that is used to verify the signature.msg
: The message that contains the user's address and the current timestamp. The message is encoded as a vector of bytes.ctx
: The transaction context that provides information about the sender, the gas limit, and the gas price.
The function performs the following steps:
- It calls the
ed25519_verify
function from thesui::ed25519
module to check if the signature is valid for the given public key and message. Theed25519_verify
function returns a boolean value that indicates the validity of the signature. - It asserts that the signature is valid, otherwise it aborts the execution with the error code
EInvalidSignature
. - It checks if the user's address exists as a key in the dynamic field of the registry object. The dynamic field is a way of storing key-value pairs in an object without declaring them in advance and it can be accessed, modified, or deleted using the
sui::dynamic_field
module. - If the user's address does not exist as a key in the dynamic field, it adds a new key-value pair to the dynamic field. The key is the user's address and the value is the expiration time of the eligibility. The expiration time is calculated by calling the
msg_to_ts
function that converts the message to a timestamp in milliseconds. - If the user's address already exists as a key in the dynamic field, it updates the value of the key to the new expiration time of the eligibility.
Now that you have implemented verify
, you can move on to the next step, which is to demonstrate how someone can interact with the contract. Write an interact
function that checks whether the user is verified or not.
// Define a public function for interacting with the registry object
public fun interact(
registry: &mut Registry, // A mutable reference to the registry object
clock: &Clock, // A reference to the clock object
ctx: &mut TxContext // A mutable reference to the transaction context
) {
// Check if there is an existing interaction history for the sender address with the registry object
if (df::exists_with_type<address, u64>(®istry.id, sender(ctx))) {
// Borrow a mutable reference to the interaction history object
let timestamp_ms = df::borrow_mut<address, u64>(&mut registry.id, sender(ctx));
// Get the current timestamp in milliseconds from the clock object
let current_timestamp = sui::clock::timestamp_ms(clock);
if (current_timestamp - *timestamp_ms <= registry.window) {
emit(
Interaction{
sender: sender(ctx),
timestamp_ms: sui::clock::timestamp_ms(clock)
}
);
} else {
abort EVerificationExpired
}
} else {
abort ENotYetVerified
}
}
The interact
function is a public function that allows the user to interact with the smart contract after passing the reCAPTCHA test. The function is linked to the verify
function, which verifies the reCAPTCHA test result and registers the user's eligibility to interact with the smart contract.
The function takes three parameters:
registry
: A mutable reference to the registry object that stores the mapping of the user's address to the expiration time of the eligibility.clock
: A reference to the clock object that provides the current timestamp in milliseconds.ctx
: A mutable reference to the transaction context that provides information about the sender, the gas limit, and the gas price.
The function performs the following steps:
- It checks if there is an existing interaction history for the sender address with the registry object. The interaction history is stored as a dynamic field in the registry object. The dynamic field is a way of storing key-value pairs in an object without declaring them in advance. The dynamic field can be accessed, modified, or deleted using the
sui::dynamic_field
module. - If there is an existing interaction history, it borrows a mutable reference to the interaction history object. The interaction history object contains the expiration time of the eligibility in milliseconds.
- It gets the current timestamp in milliseconds from the clock object and compares it with the expiration time of the eligibility. If the current timestamp is within the time window of the eligibility, it emits an interaction event. The interaction event is a struct that contains the sender address and the timestamp in milliseconds. The interaction event can be used to implement the logic of the decentralized application, such as voting, bidding, or playing.
- If the current timestamp is outside the time window of the eligibility, it aborts the execution with the error code
EVerificationExpired
. This means that the user has to pass the reCAPTCHA test again to interact with the smart contract. - If there is no existing interaction history, it aborts the execution with the error code
ENotYetVerified
. This means that the user has not passed the reCAPTCHA test yet and cannot interact with the smart contract.
And with that, your recaptcha.move
code is complete.
Deployment
See Publish a Package for a more detailed guide on publishing packages or Sui Client CLI for a complete reference of client
commands in the Sui CLI.
Before publishing your code, you must first initialize the Sui Client CLI. To do so, in a terminal or console at the root directory of the project enter sui client
. You receive the following response:
Config file ["[LINK_TO_PATH/.sui/sui_config/client.yaml"] doesn't exist, do you want to connect to a Sui Full node server [y/N]?
Enter y
to proceed. You receive the following response:
Sui Full node server URL (Defaults to Sui Devnet if not specified) :
Leave this blank (press Enter). You receive the following response:
Select key scheme to generate keypair (0 for ed25519, 1 for secp256k1, 2: for secp256r1):
Select 0
. Now you should have a Sui address set up.
Before you can publish your package to Devnet, however, you need Devnet SUI tokens. To get some, join the Sui Discord, complete the verification steps, enter the #devnet-faucet
channel and type !faucet <WALLET ADDRESS>
. For other ways to get SUI in your Devnet account, see Get SUI Tokens.
Now that you have an account with some Devnet SUI, you can deploy your contracts. To publish your package, use the following command in the same terminal or console:
sui client publish --gas-budget <GAS-BUDGET>
For the gas budget, use a standard value such as 20000000
.
The package should successfully deploy. Next, set up a backend server that verifies whether the user has successfully completed the reCAPTCHA challenge and then signs a message that should be passed to the verify
function.
Backend
To implement the backend for the reCAPTCHA, you need to create an express
app that can handle HTTP requests and responses. You also need to install some dependencies, such as @noble/ed25519
, axios
, cors
, helmet
, morgan
, and dotenv
. These packages help you with cryptography, HTTP requests, cross-origin resource sharing, security, logging, and environment variables.
Here are the steps to create the backend:
- Initialize a new project with
npm init -y
. - Install the dependencies with
npm install --save @noble/ed25519 axios cors helmet morgan dotenv
oryarn add @noble/ed25519 axios cors helmet morgan dotenv
. - Create a file named
app.ts
and paste the following code.
import * as ed from "@noble/ed25519";
import axios from "axios";
import cors from "cors";
import express from "express";
import helmet from "helmet";
import morgan from "morgan";
import api from "./api";
import MessageResponse from "./interfaces/MessageResponse";
import * as middlewares from "./middlewares";
require("dotenv").config();
const app = express();
app.use(morgan("dev"));
app.use(helmet());
app.use(cors());
app.use(express.json());
app.get<{}, MessageResponse>("/", (req, res) => {
res.json({
message: "Express + TypeScript Server",
});
});
interface RecaptchaApiResponse {
success: boolean;
challenge_ts: string; // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
hostname: string; // the hostname of the site where the reCAPTCHA was solved
signature?: Uint8Array;
pubKey?: Uint8Array;
message?: Uint8Array;
"error-codes"?: any[]; // optional
}
app.post("/verify-token", async (req, res) => {
const now: number = Date.now();
const privKey = process.env.SK!;
const pubKey = await ed.getPublicKey(privKey);
const { response, secret, userAddress } = req.body;
console.log("userAddress: " + userAddress);
console.log("secret: " + secret);
console.log("response: " + response);
console.log("now: " + now);
console.log("privKey: " + privKey);
const message: string = stringToHex(
userAddress.replace("0x", "").concat(now.toString())
);
console.log("message: " + message);
const signature = await ed.sign(message, privKey);
const isValid = await ed.verify(signature, message, pubKey);
console.log({ message, pubKey, signature, isValid });
try {
let axiosResponse = await axios.post<RecaptchaApiResponse>(
`https://www.google.com/recaptcha/api/siteverify?secret=${secret}&response=${response}`
);
console.log(axiosResponse.data);
return res.status(200).json({
success: axiosResponse.data.success,
verificationInfo: axiosResponse.data,
signature: Array.from(signature),
pubKey: Array.from(pubKey),
message: Array.from(Uint8Array.from(Buffer.from(message, "hex"))),
});
} catch (error) {
console.log(error);
return res.status(500).json({
success: false,
});
}
});
function stringToHex(str: string): string {
let hex = "";
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
const hexValue = charCode.toString(16);
// Pad with zeros to ensure two-digit representation
hex += hexValue.padStart(2, "0");
}
return hex;
}
app.use("/api/v1", api);
app.use(middlewares.notFound);
app.use(middlewares.errorHandler);
export default app;
- Examine the code to see what it does.
- First, you import the modules that you need for your app.
- Next, you create an express app and use some middlewares to enhance its functionality. You use
morgan
for logging,helmet
for security,cors
for cross-origin resource sharing, andexpress.json
for parsing JSON data. - Then, you define a
GET
route for the root path (/
) that returns a simple JSON message. - After that, you define an interface for the reCAPTCHA API response. This is the data that you receive from Google when you verify the user's response token. It contains some fields such as
success
,challenge_ts
,hostname
, anderror-codes
. It also has some optional fields that you will add later, such assignature
,pubKey
, andmessage
. - Next, you define a
POST
route for the/verify-token
path that handles the verification of the user's response token. This is the main logic of your backend. Here are the steps that you follow in this route:- Get the current time in milliseconds and store it in a variable named
now
. - Get the secret key from the environment variable
SK
and store it in a variable namedprivKey
. This is the key that you use to sign your message and verify your identity to the smart contract. - Use the
@noble/ed25519
module to get the public key from the private key and store it in a variable namedpubKey
. This is the key that you share with the smart contract and the user. - Get the response token, the secret key, and the user's address from the request body and store them in variables named
response
,secret
, anduserAddress
. - Log the values of these variables for debugging purposes.
- Create a message that consists of the user's address (without the
0x
prefix) and the current time, and convert it to a hexadecimal string. Store it in a variable namedmessage
. - Use the
@noble/ed25519
module to sign the message with the private key and store the signature in a variable namedsignature
. - Use the
@noble/ed25519
module to verify the signature with the message and the public key and store the result in a variable namedisValid
. - Log the values of these variables for debugging purposes.
- Use the axios module to send a POST request to the reCAPTCHA API with the secret key and the response token as parameters. Store the response in a variable named
axiosResponse
. - Check if the response data has a success field and if it is true. If so, return a JSON object with the following fields:
success
: trueverificationInfo
: the response data from the reCAPTCHA APIsignature
: the signature converted to an array of numberspubKey
: the public key converted to an array of numbersmessage
: the message converted to an array of numbers
- If not, catch the error and return a JSON object with the following field:
success
: false
- Get the current time in milliseconds and store it in a variable named
- Next, define a helper function named
stringToHex
that takes a string as an input and returns a hexadecimal string as an output. This function is used to convert the message to a hexadecimal format. - Finally, use some custom middlewares to handle not found and error cases, and export the app as a default module.
That's it! You have implemented the backend for the reCAPTCHA. To run the app, you can use node app.ts
or ts-node app.ts
if you have TypeScript installed. You can also use a tool like nodemon
to automatically restart the app when you make changes. To test the app, you can use a tool like Postman or curl to send requests to the app and see the responses.
Frontend
To implement the frontend for the reCAPTCHA, you need to create a react app that can render a user interface and interact with the backend and the smart contract. You also need to install some dependencies, such as @mysten/wallet-kit
, @mysten/sui.js
, axios
, and react-google-recaptcha
. These packages help you with wallet integration, transaction execution, HTTP requests, and reCAPTCHA rendering.
Here are the steps to create the frontend:
- Initialize a new project with
pnpm create vite recaptcha-app --template react-ts
. - Install the dependencies with
pnpm install --save @mysten/wallet-kit @mysten/sui.js axios react-google-recaptcha
. - Create a file named
.env
and add the following environment variables:VITE_reCAPTCHA_SITE_KEY
: the site key that you get from Google when you register your site for reCAPTCHAVITE_reCAPTCHA_SECRET_KEY
: the secret key that you get from Google when you register your site for reCAPTCHAVITE_PACKAGE_ID
: the package ID of the smart contract that you want to interact withVITE_REGISTRY_ID
: the registry ID of the smart contract that you want to interact with
- Create a file named
App.tsx
and paste the code you have provided.
import "./App.css";
import Axios from "axios";
import { ConnectButton, useWalletKit } from "@mysten/wallet-kit";
import { TransactionBlock } from "@mysten/sui.js/transactions";
import { useEffect, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui.js/utils";
interface RecaptchaApiResponse {
success: boolean;
challenge_ts: string; // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
hostname: string; // the hostname of the site where the reCAPTCHA was solved
signature?: Uint8Array;
pubKey?: Uint8Array;
message?: Uint8Array;
"error-codes"?: any[]; // optional
}
function App() {
const { currentWallet, currentAccount, signAndExecuteTransactionBlock } =
useWalletKit();
const SITE_KEY = import.meta.env.VITE_reCAPTCHA_SITE_KEY!;
const SECRET_KEY = import.meta.env.VITE_reCAPTCHA_SECRET_KEY!;
const packageId = import.meta.env.VITE_PACKAGE_ID!;
const registryId = import.meta.env.VITE_REGISTRY_ID!;
const moduleId: string = "recaptcha";
const [isRecaptchaValid, setRecaptchaValidation] = useState(false);
const [verificationPassedOneTime, setVerificationPassedOneTime] =
useState(false);
const [message, setMessage] = useState(new Uint8Array());
const [pubKey, setPubKey] = useState(new Uint8Array());
const [signature, setSignature] = useState(new Uint8Array());
const onChange = async (token: string | null) => {
if (token === null) {
setRecaptchaValidation(false);
} else {
const recaptchaApiResponse: RecaptchaApiResponse = await verifyToken(
token
);
setRecaptchaValidation(true);
if (!verificationPassedOneTime) setVerificationPassedOneTime(true);
if (recaptchaApiResponse.message !== undefined)
setMessage(recaptchaApiResponse.message);
if (recaptchaApiResponse.pubKey !== undefined)
setPubKey(recaptchaApiResponse.pubKey);
if (recaptchaApiResponse.signature !== undefined)
setSignature(recaptchaApiResponse.signature);
}
};
async function verifyToken(token: string): Promise<RecaptchaApiResponse> {
try {
const response = await Axios.post(
`https://bot-prevention-api.vercel.app/verify-token`,
{
response: token,
secret: SECRET_KEY,
userAddress: currentAccount?.address,
}
);
return response["data"];
} catch (error) {
console.log(error);
}
return {} as RecaptchaApiResponse;
}
useEffect(() => {
// You can do something with `currentWallet` here.
}, [currentWallet]);
return (
<div className="App">
<ConnectButton />
<div>
<button
disabled={!verificationPassedOneTime}
onClick={async () => {
const transactionBlock = new TransactionBlock();
transactionBlock.moveCall({
target: `${packageId}::${moduleId}::interact`,
arguments: [
transactionBlock.object(registryId),
transactionBlock.object(SUI_CLOCK_OBJECT_ID),
],
});
console.log(
await signAndExecuteTransactionBlock({
transactionBlock: transactionBlock,
options: { showEffects: true },
})
);
}}
>
Interact
</button>
</div>
<div>
<button
disabled={!isRecaptchaValid}
onClick={async () => {
const transactionBlock = new TransactionBlock();
transactionBlock.moveCall({
target: `${packageId}::${moduleId}::verify`,
arguments: [
transactionBlock.object(registryId),
transactionBlock.pure(signature),
transactionBlock.pure(pubKey),
transactionBlock.pure(message),
],
});
console.log(
await signAndExecuteTransactionBlock({
transactionBlock,
options: { showEffects: true },
})
);
}}
>
Verify
</button>
</div>
<hr />
<ReCAPTCHA sitekey={SITE_KEY} onChange={onChange} />
</div>
);
}
export default App;
- Examine the code to see what it does.
- First, import the modules that you need for your app.
- Next, use the
useWalletKit
hook from@mysten/wallet-kit
to get access to the current wallet, the current account, and thesignAndExecuteTransactionBlock
function. These help you connect to the wallet and execute transactions on the blockchain. - Then, define some constants for the site key, the secret key, the package ID, the registry ID, and the module ID. These are the values that you use to interact with the reCAPTCHA API and the smart contract.
- After that, define some state variables to store the status of the reCAPTCHA validation, the verification result, and the message, the public key, and the signature that you get from the backend. Use the
useState
hook from React to manage these state variables. - Next, define a function named
onChange
that takes a token as an input and handles the change of the reCAPTCHA component. This function is triggered when the user completes the reCAPTCHA challenge. Here are the steps that you follow in this function:- Check if the token is null. If so, set the reCAPTCHA validation state to false.
- If not, call the
verifyToken
function with the token as an argument and store the result in a variable namedrecaptchaApiResponse
. This function sends a POST request to the backend and gets the verification result and the data that you need to interact with the smart contract. - Set the reCAPTCHA validation state to true.
- Check if the
verificationPassedOneTime
state is false. If so, set it to true. This state is used to enable the interact button only once after the user passes the verification. - Check if the
recaptchaApiResponse
has the message, thepubKey
, and thesignature
fields. If so, set the corresponding state variables with the values from the response.
- Next, define a function named
verifyToken
that takes a token as an input and returns a promise of the reCAPTCHA API response. This function is used to communicate with the backend. Here are the steps that you follow in this function:- Try to send a POST request to the backend URL with the token, the secret key, and the user's address as the body parameters. Use the
axios
module to send the request and store the response in a variable namedresponse
. - Return the data field of the response as the reCAPTCHA API response.
- Catch any error and log it to the console.
- Return an empty object as the default reCAPTCHA API response.
- Try to send a POST request to the backend URL with the token, the secret key, and the user's address as the body parameters. Use the
- Next, use the
useEffect
hook from React to run some code when thecurrentWallet
state changes. In this case, you don't do anything, but you could add some logic here if you want to. - Finally, return a JSX element that renders the app. The app consists of the following components:
- A
ConnectButton
component from@mysten/wallet-kit
that allows the user to connect to their wallet. - A button that allows the user to interact with the smart contract. This button is disabled unless the user passes the verification at least once. When the user clicks this button, create a new
TransactionBlock
object from@mysten/sui.js
and add amoveCall
action that calls theinteract
function of the smart contract with the registry ID and the clock object ID as arguments. Then, use thesignAndExecuteTransactionBlock
function from@mysten/wallet-kit
to sign and execute the transaction block on the blockchain. You also log the result to the console. - A button that allows the user to verify their identity to the smart contract. This button is disabled unless the user passes the reCAPTCHA challenge. When the user clicks this button, create a new
TransactionBlock
object from@mysten/sui.js
and add amoveCall
action that calls the verify function of the smart contract with the registry ID, the signature, the public key, and the message as arguments. Then, use thesignAndExecuteTransactionBlock
function from@mysten/wallet-kit
to sign and execute the transaction block on the blockchain. You also log the result to the console. - A
ReCAPTCHA
component fromreact-google-recaptcha
that renders the reCAPTCHA widget. You pass the site key and theonChange
function as props to this component.
- A
That's it! You have implemented the frontend for the reCAPTCHA. To run the app, you can use pnpm run dev
. To test the app, you can open the browser and go to the localhost:5173
URL. You should see the app and can interact with the reCAPTCHA and the smart contract.