This tutorial will guide you through the process of building an authentication system based on JSON Web Tokens (JWT) using NestJS, a progressive Node.js framework. JWT is a widely adopted and standardized approach for securely exchanging data between different entities. It is commonly employed in web applications for tasks such as authentication and authorization.
Prisma: Prisma is an ORM (Object-Relational Mapping) tool that we will use to interact with the database. It provides an intuitive and efficient way to perform database operations, such as querying, creating, updating, and deleting records, using a type-safe API.
'@types/.*': These packages provide the necessary type definitions for external libraries or modules in your TypeScript code. They enable type checking and provide IntelliSense support, enhancing the development experience and helping to prevent type-related errors.
@nestjs/jwt: We will utilize this NestJS package to implement JWT-based authentication, ensuring secure transmission of information between parties.
bcrypt: To enhance security, we will employ this package for hashing passwords, adding an extra layer of protection to user credentials.
@prisma/client: We will leverage this database client in conjunction with the Prisma ORM to interact with the database, benefiting from a type-safe API for efficient and reliable database queries.
class-validator: By utilizing decorators provided by this package, we can easily define and enforce validation constraints for the properties of our classes, ensuring data integrity and consistency.
class-transformer: This package allows us to effortlessly convert plain JavaScript or TypeScript objects into instances of classes, enabling convenient manipulation and transformation of data.
uuid: To generate universally unique identifiers (UUIDs), we will rely on this package, providing us with reliable and distinct identifiers for various purposes.
cookie-parser: Implementing this middleware will enable us to parse the Cookie header and conveniently access the cookie data through the req.cookies object, simplifying cookie handling and management.
To install these packages, run the following command:
We define our models inside a prisma file. To generate it, run the following command:
npx prisma init --datasource-provider mongodb
This command initializes a prisma/schema.prisma and a .env file.
In the .env file, set the DATABASE_URL variable with your mongodb connection URL.
We’re ready to create a User model using Prisma.
Inside the prisma/schema.prisma file, add the following model:
model User { id String @id @default(auto()) @map("_id") @db.ObjectId email String @unique password String }
id: unique identifier for the user (it is mapped to _id in the DB)
email: field for the user's email address
password: field used to store the user's hashed password
Now, let’s move on the RevokedToken collection. You can add it after the User model:
model RevokedToken{ id String@id @default(auto()) @map("_id")@db.ObjectId jti String@unique }
jti: the uuid of the token
To push your models to your database, run the following command:
yarn prisma db push
This will also generate the Prisma Client, an auto-generated and type-safe query builder that's tailored to your data.
caution
Every time you make changes to your prisma/shema.prisma file, always remember to push the changes to your database to regenerate an updated Prisma Client.
When setting up your NestJS application, you'll want to abstract away the Prisma Client API for database queries within a service. To get started, you can create a new PrismaService that takes care of instantiating PrismaClient and connecting to your database.
Run this command to create a PrismaService at src/prisma/prisma.service.ts:
nest g service prisma
Create a new file called src/prisma/prisma.service.ts and update the code in it to look like this:
Now that we have a PrismaService, we can export it in a PrismaModule, to be able to import it in every other module where we need to interact with the database.
Now that we have installed the necessary packages, set up the User model and generated the Prisma Client we can create a user resource in our API.
In your terminal enter the following command to create a Nest user resource:
nest g resource user
In the NestCLI, choose REST API, then, n to generate CRUD entry points - we will create them by ourselves -
This command should have created a src/user directory, with 5 files inside.
Files ending by .spec.ts are unit test files, we will see how to set them up in another tutorial. For now, we will focus on (expand for more):
user.module.ts
A NestJS module is a self-contained unit of code that encapsulates a specific feature or functionality of an application. It provides a way to organize and structure code into reusable and maintainable units. Typically, modules encapsulate controllers and services related to a specific resource, in this example, a user.
user.controller.ts
A NestJS controller is a class that defines routes or endpoints of a web application. It handles incoming requests from clients by executing corresponding methods or functions and returning responses. Controllers group routes related to a certain resource together, and are part of the communication layer.
In this file, we will define all the routes related to a user.
user.service.ts
A NestJS service is a class that contains business logic and functionality that can be shared across different parts of an application. It provides a way to encapsulate and separate concerns related to data access, manipulation, and transformation from the rest of the application logic. This layer is responsible to interact with the data layer.
In the context of a NestJS application, we can think of a layered architecture where each layer serves a specific purpose and has its own set of responsibilities.
At the highest level, we have the Controllers layer, which handles incoming requests and orchestrates the flow of control in the application by invoking corresponding methods or functions in the underlying Services layer.
The Services layer encapsulates the business logic and interacts with the underlying Data Access layer to perform CRUD operations on the database or external APIs.
By separating concerns into these layers, we can achieve a modular and scalable architecture that promotes code reusability, maintainability, and testability.
Importing and configuring the Json web token Module
ABOUT JWT
JWT stands for JSON Web Token, which is a compact, self-contained mechanism for securely transmitting information between parties as a JSON object. JWTs are often used for authentication and authorization purposes in web applications.
NestJS provides a JwtModule that uses jsonwebtoken under-the-hood. We can import it in our UserModule to be able to use utility functions related to JWTs.
Open src/app.module.ts and update it to look like this:
We configure the JwtModule using register(), passing in a configuration object. It contains the secret we use to sign issued JWTs and their lifetime duration.
See here for more on the Nest JwtModule and here for more details on the available configuration options.
info
💡 To generate a strong secret, you can run the following command in your terminal:
Now, let's create an authentication controller to handle authentication requests such as registration, sign-in and sign-out. Open src/user/auth.controller.ts and add the following code:
💡 Guards determines if the request will be handled or not by the controller / route it protects
Here we use a guard to check if the client is authenticated, and attach a user object to the request. The user object is the payload of the JWT. Thus, protected routes have access to the user via the request if they need to.
But there is a problem: the express request object don’t have a user property. So we have to customize the Request object a little bit. Create the src/types/express/index.d.ts and add the following code to it:
import{JwtPayload}from'src/contracts/jwt-payload/jwt-payload.interface'; declare global { namespaceExpress{ exportinterfaceRequest{ user:JwtPayload; } } }
We also need to create the JwtPayload interface. Run this command to create src/contracts/jwt-payload/jwt-payload.interface.ts:
Now we can write our AuthGuard. To do so, run the following command and open the generated src/guards/auth/auth.guard.ts:
nest g guard guards/auth
Update it to make it like that:
import{ CanActivate, ExecutionContext, Injectable, UnauthorizedException, }from'@nestjs/common'; import{JwtService}from'@nestjs/jwt'; import{Request}from'express'; import{PrismaService}from'src/prisma/prisma.service'; import{JwtPayload}from'src/contracts/jwt-payload/jwt-payload.interface'; @Injectable() exportclassAuthGuardimplementsCanActivate{ constructor( privatereadonly jwtService:JwtService, privatereadonly prisma:PrismaService, ){} asynccanActivate(context:ExecutionContext):Promise<boolean>{ try{ // Try to retrieve the JWT from request's cookies //-------------------------------------------------------------------------- const request:Request= context.switchToHttp().getRequest(); const token:string= request.cookies['jwt']; if(!token)thrownewUnauthorizedException(); // Verify the JWT and check if it has been revoked //-------------------------------------------------------------------------- const payload:JwtPayload=awaitthis.jwtService.verifyAsync( request.cookies['jwt'], { secret: process.env.JWT_SECRET}, ); if( awaitthis.prisma.revokedToken.findUnique({ where:{ jti: payload.jti}, }) ) thrownewUnauthorizedException(); // Attach user's data to the request //-------------------------------------------------------------------------- request.user= payload; returntrue; }catch(err:unknown){ thrownewUnauthorizedException(); } } }
We can now implement the business logic of these routes. In order to do so, we will create a UserService. It will handle user authentication and token generation.
The signUp method is responsible for creating a new user in the database. It utilizes the create method of the prisma.user object to store the user's information. Additionally, it generates a JSON Web Token (JWT) using the signAsync method from the JwtService object, which is provided by the @nestjs/jwt package. This method returns both the user object and the corresponding token.
The signIn method verifies if a user with the specified email exists in the database. It achieves this by utilizing the findUnique method of the prisma.user object. If the user is found, a JWT token is generated for authentication using the signAsync method from the JwtService object. The method then returns the user object along with the associated token.
The revokeToken method invalidates a JWT token by adding it to the revokedToken collection in the database. This is accomplished by utilizing the create method of the prisma.revokedToken object. The method returns a boolean value indicating the success or failure of the token revocation process.
The getById method retrieves a user from the database based on their unique ID. If the user does not exist, an exception is thrown. This method is primarily utilized in the getMe method of the UserController to fetch the user's data based on the JWT payload.
Thank you for following this tutorial. Together, we learned to create a JWT-based authentication system with NestJS. We set up a Prisma service and generated a Prisma Client, created a user resource with a layered architecture, configured the JwtModule, and implemented an authentication controller. We also explored Guards for route protection, built a user service, and conducted thorough testing.
In the last tutorial our users registered themselves to our app with an email and a password. Since we want to monitor when they receive an NFT, we need to ask them for their public key at registration.
Create a new SignUp.dto.ts in the src/user directory:
This DTO includes a public address field, which allows users to provide their public EVM address. We will use this address to identify when a user receives a new NFT.
First, we define an axios instance as a member of the StartonService class. This is convenient because we can configure the instance with a baseURL and headers, which prevents us from repeating ourselves in subsequent code.
Next, we define three wrappers for common API calls: transfers, mints, and burns:
We will encapsulate the functions responsible for sending emails in a service that will be exported from the EmailModule.
nest g service email nest g module email
import{Injectable}from'@nestjs/common'; import{ createTransport }from'nodemailer'; /* |-------------------------------------------------------------------------- | MAILING SERVICE |-------------------------------------------------------------------------- */ @Injectable() exportclassEmailService{ // Utility object to send emails //-------------------------------------------------------------------------- privatereadonly _transporter =createTransport( { host: process.env.EMAIL_HOST,// smtp.elasticemail.com port: process.env.EMAIL_PORT,// default to 2525 with elasticemail auth:{ user: process.env.EMAIL_USER,// The email address that sends the message pass: process.env.EMAIL_PASS,// The password of the email address }, }, { from:`Cryptomancy <${process.env.EMAIL_USER}>`,// "from" field of the message }, ); // Verify SMTP configuration at construction //-------------------------------------------------------------------------- constructor(){ this._transporter.verify().then(()=>{ console.log('Ready to send emails'); }); } // Send an email //-------------------------------------------------------------------------- asyncsendEmail(to:string, subject:string, text:string){ awaitthis._transporter.sendMail({ to, subject, text }); } }
To send emails, we use nodemailer, a package that abstracts SMTP to make it easier to send emails. The first step is to define a transporter, which is responsible for transmitting emails. This can be done by calling the createTransport function and passing a configuration object as an argument to it. For this example we use smtp.elasticemail.com as host. You can create an account and generate SMTP credentials for free on their website.
In the constructor, we call the verify method to check if we are authenticated correctly and that the server is ready to accept messages.
We also define a single method, sendEmail, which takes a recipient email address, subject, and message body as parameters and send the email.
Now that we have all the necessary utility services, we need services for adding and updating items in the database.
Run the following command to generate the ItemService under src/item/item.service.ts:
nest g service item
Update it to look like this:
/* | Developed by Starton | Filename : item.service.ts | Author : Alexandre Schaffner ([email protected]) */ import{Injectable}from'@nestjs/common'; import{Prisma}from'@prisma/client'; import{PrismaService}from'src/prisma/prisma.service'; import{ nullAddress }from'src/utils/constants'; /* |-------------------------------------------------------------------------- | ITEM SERVICE |-------------------------------------------------------------------------- */ @Injectable() exportclassItemService{ constructor(privatereadonly prisma:PrismaService){} asynccreate(data:Prisma.ItemCreateInput){ awaitthis.prisma.item.create({ data }); } asyncupdateByTokenId(tokenId:string, data:Prisma.ItemUpdateInput){ awaitthis.prisma.item.update({ where:{ tokenId }, data }); } asyncdeleteByTokenId(tokenId:string){ awaitthis.prisma.item.delete({ where:{ tokenId }}); } asyncsafeTransferFrom( collection:string, tokenId:string, from:string, to:string, ){ // Add item to database in case of a mint (from === nullAddress) //-------------------------------------------------------------------------- if(from === nullAddress){ awaitthis.prisma.item.create({ data:{ collection:{ connect:{ contractAddress: collection, }, }, tokenId, ownerAddress: to, }, }); // Increment nextTokenId in the database //-------------------------------------------------------------------------- awaitthis.prisma.collection.update({ where:{ contractAddress: collection, }, data:{ nextTokenId:{ increment:1, }, }, }); // Update item's owner in case of a transfer //-------------------------------------------------------------------------- }else{ awaitthis.prisma.item.updateMany({ where:{ AND:[ { tokenId }, { collection:{ contractAddress: collection }}, { ownerAddress: from }, ], }, data:{ ownerAddress: to, }, }); } } }
The ItemService is responsible for managing items in the database. It provides several methods:
create(data: Prisma.ItemCreateInput): adds a new item to the database.
updateByTokenId(tokenId: string, data: Prisma.ItemUpdateInput): updates an existing item in the database based on its token ID.
deleteByTokenId(tokenId: string): removes an item from the database based on its token ID.
safeTransferFrom(collection: string, tokenId: string, from: string, to: string): adds or updates an item in the database depending on whether it was transferred or minted. If the from address is nullAddress, it creates a new item in the database (mint). Otherwise, it updates the item's owner in the database.
/* | Developed by Starton | Filename : transfer.service.ts | Author : Alexandre Schaffner ([email protected]) */ import{Injectable}from'@nestjs/common'; import{Prisma}from'@prisma/client'; import{PrismaService}from'src/prisma/prisma.service'; /* |-------------------------------------------------------------------------- | TRANSFER SERVICE |-------------------------------------------------------------------------- */ @Injectable() exportclassTransferService{ constructor(privatereadonly prisma:PrismaService){} // Create a transfer record in the database //-------------------------------------------------------------------------- asynccreate(transfer:Prisma.TransferCreateInput){ awaitthis.prisma.transfer.create({ data: transfer }); } }
The create method adds a new record to the Transfer collection of the database. We will call this method every time a transfer happens.
For our app to be notified of a Transfer event, we need to set up a Watcher on Starton. A Watcher is a condition that is checked upon inspection of each block. When the watcher is triggered, it sends a POST request to a webhook containing data about the event. You can find how to create a Watcher here.
/* | Developed by Starton | Filename : transfer.controller.ts | Author : Alexandre Schaffner ([email protected]) */ import{ Body, Controller, InternalServerErrorException, Post, UseGuards, }from'@nestjs/common'; import{Prisma}from'@prisma/client'; import{MintDto}from'src/contracts/dto/Mint.dto'; import{SafeTransferDto}from'src/contracts/dto/SafeTransfer.dto'; import{AuthGuard}from'src/guards/auth/auth.guard'; import{ItemService}from'src/item/item.service'; import{StartonService}from'src/starton/starton.service'; import{UserService}from'src/user/user.service'; import{ cryptoquartzCollectionAddress }from'src/utils/constants'; import{EmailService}from'../email/email.service'; import{TransferService}from'./transfer.service'; import{StartonGuard}from'src/guards/starton/starton.guard'; /* |-------------------------------------------------------------------------- | TRANSFER CONTROLLER |-------------------------------------------------------------------------- */ @Controller('transfer') exportclassTransferController{ constructor( privatereadonly transferService:TransferService, privatereadonly starton:StartonService, privatereadonly userService:UserService, privatereadonly itemService:ItemService, privatereadonly emailService:EmailService, ){} /* |-------------------------------------------------------------------------- | WEBHOOK ENDPOINT TRIGGERED BY STARTON |-------------------------------------------------------------------------- */ @UseGuards(StartonGuard) @Post('webhook') asyncwebhook(@Body() body:any){ try{ const{ from, to, id }= body.data.transferSingle; const transfer:Prisma.TransferCreateInput={ item:{ connect:{ tokenId: id.hex.toLowerCase()}}, from: from.toLowerCase(), to: to.toLowerCase(), toUser:{ connect:{ publicAddress: to.toLowerCase()}}, fromUser:{ connect:{ publicAddress: from.toLowerCase()}}, txHash: body.data.transaction.hash.toLowerCase(), }; // Check if user exists, if not, don't connect records //-------------------------------------------------------------------------- const toUser =awaitthis.userService.findByPublicAddress( to.toLowerCase(), ); if(!toUser)delete transfer.toUser; const fromUser =awaitthis.userService.findByPublicAddress( from.toLowerCase(), ); if(!fromUser)delete transfer.fromUser; // Change the owner of the item in the database //-------------------------------------------------------------------------- awaitthis.itemService.safeTransferFrom( cryptoquartzCollectionAddress, id.hex.toLowerCase(), from.toLowerCase(), to.toLowerCase(), ); // Create the transfer record //-------------------------------------------------------------------------- awaitthis.transferService.create(transfer); // If the recipient is a user, send an email //-------------------------------------------------------------------------- if(!toUser)return; // Use a template here //-------------------------------------------------------------------------- awaitthis.emailService.sendEmail( toUser.email, 'NFT Transfer', 'The address '+ from + ' sent the NFT #'+ id.hex+ ' to your address '+ to + '.', ); return; }catch(err:unknown){ console.error(err); thrownewInternalServerErrorException(); } } }
So, we need a route to handle requests sent by Starton.
We create a /transfer/webhook endpoint. In it, we create a Transfer record from the data we received in the body and check if addresses in the Transfer record are related to a user.
Then, we call itemService's safeTransferFrom method to update the Item’s owner in the database and we create a new Transfer record in the DB via transferService's create method.
Finally, we use the emailService we coded previously to send an email to the receiver of the NFT if he is a registered user.
Note that the endpoint is protected by the StartonGuard, which we will focus later on.
We can now be notified of transfers via the /transfer/webhook endpoint. This is good, but we need to secure the endpoint so that only Starton can trigger it.
To do so we create a StartonGuard:
nest g guard guards/starton
/* | Developed by Starton | Filename : starton.guard.ts | Author : Alexandre Schaffner ([email protected]) */ import{CanActivate,ExecutionContext,Injectable}from'@nestjs/common'; import{ createHmac }from'crypto'; import{Request}from'express'; /* |-------------------------------------------------------------------------- | STARTON'S SIGNATURE VERIFICATION GUARD |-------------------------------------------------------------------------- */ @Injectable() exportclassStartonGuardimplementsCanActivate{ canActivate(context:ExecutionContext):boolean{ const request:Request= context.switchToHttp().getRequest(); const payload =JSON.stringify(request.body); const reqSignature = request.get('starton-signature'); if(!reqSignature)returnfalse; // Re-compute the signature and compare it with the one received //-------------------------------------------------------------------------- const localSignature =createHmac( 'sha256', process.env.STARTON_SECRETasstring, ) .update(Buffer.from(payload)) .digest('hex'); return reqSignature === localSignature; } }
In it, we retrieve the Request object, the payload of the body and the signature. Then, we compute the signature again using the payload and the signing key (you can find yours in your Starton’s dashboard under: Your project > Developer > Webhook). Finally we compare the signature we computed with the one provided in the header of the request we retrieved previously. If they match, we allow access to the endpoint, and if not it responds with an error.
Next, we add endpoints to allow minting, burning and transfers of items.
/* |-------------------------------------------------------------------------- | TRANSFER / MINT / BURN ENDPOINTS |-------------------------------------------------------------------------- */ // safeTransferFrom //-------------------------------------------------------------------------- @UseGuards(AuthGuard) @Post() asyncsafeTransferFrom(@Body() safeTransferDto:SafeTransferDto){ awaitthis.starton.initTransfer( safeTransferDto.from, safeTransferDto.to, safeTransferDto.tokenId, ); } // Mint a token //-------------------------------------------------------------------------- @UseGuards(AuthGuard) @Post('mint') asyncmint(@Body() mintDto:MintDto){ awaitthis.starton.initMint(mintDto.to, mintDto.tokenId); } // Burn a token //-------------------------------------------------------------------------- @UseGuards(AuthGuard) @Post('burn') asyncburn(@Body() burnDto:MintDto){ awaitthis.starton.initBurn(burnDto.to, burnDto.tokenId); awaitthis.itemService.deleteByTokenId(burnDto.tokenId); }
All the 3 endpoints calls methods of the Starton wrapper service. Thus, Starton can make the smart contract function calls to apply changes on-chain. Every call to this endpoint ends up in a Transfer event on-chain, which triggers our webhook and applies changes to the database.
You successfully created an email notification system that is triggered every time a transfer occurs on your ERC1155 NFTs collection !
You have learned how to create an email module to automatically send emails with Nodemailer, how you can use a Watcher with a webhook to track for specific event on the blockchain.
Now that you have successfully created an email notification system that sends emails whenever a transfer occurs on your ERC1155 NFTs collection, you can customize the email template to make it more appealing to your users. You can also explore other notification methods such as SMS or push notifications to provide your users with more options. Additionally, you may want to consider implementing more security features like roles to ensure that your endpoints are properly secured against unauthorized access.
With Starton, you can monitor blockchain activity using watchers. Whether it's to watch the activity of a wallet or a smart contract, you can create watchers, triggered by events.
not a developer?
To follow this tutorial, you will need coding experience. If it's not your case, you can still create a watcher:
A watcher is a powerful tool that lets you monitor blockchain events in real-time.
With Starton, you can set up watchers to monitor activities such as address activities, balance changes, or even smart contract events.
Set up the watcher parameters
// Let's create your watcher starton .post("/v3/watcher",{ name:"Name of your watcher", type:"ADDRESS_ACTIVITY", address:"0x000000000000", network:"polygon-amoy", webhookUrl:"https://xxxxxxx/", confirmationsBlocks:50, }) .then((response)=>{ console.log(response.data) }) .catch((error)=>console.error(error.response.data))
Let's go through the parameters provided to create a watcher:
name: This field specifies the name of your watcher. Useful for identification purposes, especially if you have multiple watchers set up for the same address or contract at different confirmation speeds different confirmation speeds.
type: The type of blockchain event that you want the watcher to monitor. The ADDRESS_ACTIVITY type, in this context, means the watcher will monitor and notify of any activity related to the provided address but Starton provides you with a list of events as well as the possibility to watch events from your own custom contract .
address: This is the specific blockchain address you want to monitor. It can be a wallet address or a smart contract address, depending on the event type you chose.
network: This specifies the blockchain network on which is the given address. In the provided example, polygon-amoy refers to the amoy testnet of the Polygon (previously known as Matic) network. For more, see Supported Networks.
webhookUrl: This URL is where the notifications will be sent when the watched event occurs. Essentially, once the watcher detects the specified event on the blockchain, it will send an HTTP POST request to this URL with details of the event. You can use https://webhook.site/ or test a webhook on your local environment using ngrok.
confirmationsBlocks: This is the number of confirmed blocks before an event triggers the watcher. It's a safety measure to ensure that the event (like a transaction) is well-confirmed on the blockchain and is not likely to be reversed. The higher the number, the more confirmations are required, and vice versa. Each network has its own default confirmation blocks number, for more information, see Understanding confirmation blocks.
Almost done! Now we can execute it to get our first watcher!
node list.js
And just like that, your first watcher is now live on Starton!
When Starton sends a notification, Monitor sends an HTTP request to a web server.
If your server is actually localhost, the request will fail. Starton cannot actually reach localhost.
We need a tool to make that possible. The tool we're using is called ngrok.
You can use this Webhook URL in Starton to create your watcher.
From Code
From Webapp
To create a watcher from the code of you application, use the following snippet.
You can find the full list of networks and event types in our API reference.
const axios =require("axios"); // First, authenticate to the API const startonApi = axios.create({ baseURL:"https://api.starton.com", headers:{ "x-api-key":"YOUR_API_KEY", }, }); // Use the watcher creation endpoint startonApi.post( "/v3/watcher", { name:"Name of your watcher",// Enter a name for your watcher description:"Describe your watcher",// Enter a description for your watcher address:"0x000000000000",// Enter the address to watch (either a wallet or a smart contract) network:"polygon-amoy",// Enter the network of your address. type:"ADDRESS_ACTIVITY",// Select an event type webhookUrl:"https://xxxxxxx/",// Here, enter the webhook url you got from ngrok. confirmationsBlocks:50// Depending on your needs, select the number of confirmed blocks before an event triggers your watcher } ).then((response)=>{ console.log(response.data) })
From Dashboard, go to Watcher.
Click + Watcher.
Enter a Name and Description.
Click Next.
Select a type of notification.
Fill in the Settings.
In Construction:
Select a Blockchain.
Select a Network.
Enter an address
Enter a webhook URL. Here, enter the webhook url you got from ngrok.
In this tutorial, we will create a tracker for wallet with Starton and Zapier. We will start by creating a webhook on Zapier, adding the URL in a watcher on Starton, to populate a Googlesheets on Google Drive.
Select a Type of notification and click Next. You can use Address Activity to track when an address receives base currency or creates a transaction. For more information, check out Available event types.
Enter the watcher's Name and Description. Click Next.
Select the blockchain on which you want to track the wallet (for example, we use Binance Smart Chain testnet ).
Enter the Wallet Address that you need to track.
Copy the URL and paste in Webhook URL.
Enter the number of Confirmations Blocks to wait before receiving the notification. More information on confirmation blocks.
For this example we use BSC Testnet, so we use 48 confirmation blocks.