Skip to main content

6 posts tagged with "MONITOR"

View All Tags

· 2 min read
Loubna Benzaama

Using Monitor might be very useful to receive webhooks whenever specific events happen on-chain.

However, we need to be careful when receiving webhooks: someone could try to send a webhook to your endpoint, pretending to be Starton.

To protect our users against this kind of attacks we provide a signature within the request’s headers.

You can then compare this signature to the one you get when signing the payload yourself.

Let’s see in details the process of verifying the signature with some JS example.

Get the signature in the header

The signature is an hexadecimal string that looks like ac7c2691a5f632ad4fc2f8b225fd6f25e3526f12293c32efbcb5999527746037.

You can find it in the headers with the key starton-signature.

const reqSignature = req.headers["starton-signature"]

Find your signing key on Starton

To be able to sign the message you'll need to use the same signing key that was used to get the starton-signature.

This webhook signing key is available in the Developer section of our dashboard.

It should look like: whs_lDtcN3gDEaY_Yg3aony8077Lr-9wa5JyxAitUvfg886y9JDs4mVoY9uD.

Sign the payload using the signing key

To properly compute the signature you will have to compute an HMAC with the SHA256 hash function.

You must provide the signing key as the secret key and the full payload you received as the message to sign.

If you code in JS / TS you can use the node-forge module to compute the HMAC.

import { hmac } from "node-forge";

const secretKey = "whs_lDtcN3gDEaY_Yg3aony8077Lr-9wa5JyxAitUvfg886y9JDs4mVoY9uD";
const payload = { ... }

const stream = hmac.create();
stream.start("sha256", secretKey);
stream.update(Buffer.from(JSON.stringify(payload)));

const localSignature = stream.digest().toHex();
console.log(localSignature);

Check that your signature is the same as the one provided in the header

Great! We can now check if the signature sent in the webhook matches the one we computed locally.

if (!reqSignature || localSignature !== reqSignature) {
throw "Signature doesn't match"
}

And that's it! You should now be able to check and guarantee that the webhooks are coming from Starton and not from an evil mind!

· 16 min read
Loubna Benzaama

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.

Find the full project in our Github repository.

Prerequisites

Before we begin, make sure that you have the following installed on your machine:

  • Node.js (version 18 or higher)

  • NestJS CLI

    yarn global add @nestjs/cli

We will use MongoDB for our database. You can create a free instance on Mongo Atlas. Make sure to retrieve the connection URL from your dashboard.

Creating a new NestJS project

First, let's create a new NestJS project using the Nest CLI. Open your terminal and run the following command:

nest new jwt-auth-tuto --strict

This will create a new NestJS project in a directory named jwt-auth-tuto.

Installing packages

Next, we need to install the following packages:

Development dependencies

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.

To install these development packages:

  1. Go to jwt-auth-tuto,
  2. Run the following command:
yarn add -D prisma @types/uuid @types/bcrypt @types/cookie-parser

Dependencies

  • @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:

yarn add @nestjs/jwt bcrypt @prisma/client class-validator class-transformer uuid cookie-parser

Prisma Models

Creating Prisma models

info

A model is a representation of a collection

We need 2 models for our app:

  • A User model to store users data
  • A RevokedToken model to store revoked JWT

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.

Wrapping the 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:

import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}

async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

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.

Create a PrsimaModule by running this command:

nest g module prisma

Then, update src/prisma/prisma.module.ts:

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

Creating a user resource

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:

import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
import { PrismaService } from './prisma/prisma.service';
import { JwtModule } from '@nestjs/jwt';

@Module({
imports: [
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '2h' },
}),
UserModule,
],
controllers: [],
providers: [PrismaService],
})
export class AppModule {}

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:

node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"

Once you have generated a strong secret, add it to your .env file as JWT_SECRET

Creating authentication endpoints

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:

import {
Body,
Controller,
InternalServerErrorException,
Post,
Get,
Res,
UseGuards,
Req,
UseInterceptors,
} from '@nestjs/common';

import { UserService } from './user.service';
import { SignInDto } from './dto/SignIn.dto';
import { Request, Response } from 'express';
import { User } from '@prisma/client';
import { AuthGuard } from 'src/guards/auth/auth.guard';
import { RemovePasswordInterceptor } from 'src/interceptors/remove-password/remove-password.interceptor';

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Post('auth/sign-up')
async signUp(@Body() newUser: SignInDto, @Res() res: Response): Promise<void> {
try {
const { user, token }: { user: Partial<User>; token: string } =
await this.userService.signUp(newUser);

delete user.password;

res.cookie('jwt', token, { httpOnly: true });

res.status(201).send({ user: user });
} catch (err: unknown) {
throw new InternalServerErrorException(err);
}
}

@Post('auth/sign-in')
async signIn(@Body() credentials: SignInDto, @Res() res: Response) {
try {
const { user, token }: { user: Partial<User>; token: string } =
await this.userService.signIn(credentials);

delete user.password;

res.cookie('jwt', token, { httpOnly: true });

res.send({ user: user });
} catch (err: unknown) {
throw new InternalServerErrorException(err);
}
}

@UseGuards(AuthGuard)
@Post('auth/revoke-token')
async revokeToken(@Req() req: Request): Promise<{ revoked: boolean }> {
return { revoked: await this.userService.revokeToken(req.user.jti) };
}

@UseGuards(AuthGuard)
@UseInterceptors(RemovePasswordInterceptor)
@Get()
async getMe(@Req() req: Request): Promise<{ user: User }> {
try {
return { user: await this.userService.getById(req.user.sub) };
} catch (err: unknown) {
throw new InternalServerErrorException(err);
}
}
}

This code defines a UserController with different methods to handle authentication requests:

  • signUp: called by a POST request to /user/auth/sign-up to register new users and generate a JWT token
  • signIn: called by a POST request to /user/auth/sign-in to authenticate users and generate a JWT token
  • revokeToken: called by a POST request to /user/auth/revoke-token to revoke an existing JWT token

Sign-up and sign-in routes require a body with the shape of a SignInDto

info

DTO stands for Data Transfer Objects. They are defined by classes and are useful to validate inputs.

Create a src/user/dto/SignIn.dto.ts file and add the following code to it:

import { IsEmail, IsStrongPassword } from 'class-validator';

export class SignInDto {
@IsEmail()
email: string;

@IsStrongPassword()
password: string;
}

The SignInDto contains an email and a password. We use @IsEmail() and @IsStrongPassword() decorators to validate the data.

To enable validation and cookie parsing, update your src/main.ts like this:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as cookieParser from 'cookie-parser';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.use(cookieParser());
app.useGlobalPipes(new ValidationPipe());

await app.listen(3000);
}
bootstrap();

AuthGuard

The revoke-token route is protected by a guard.

info

💡 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 {
namespace Express {
export interface Request {
user: JwtPayload;
}
}
}

We also need to create the JwtPayload interface. Run this command to create src/contracts/jwt-payload/jwt-payload.interface.ts:

nest g interface contracts/jwt-payload

Update it to make it look like:

export interface JwtPayload {
sub: string;
exp: number;
jti: string;
iat: number;
}

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()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly prisma: PrismaService,
) {}

async canActivate(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) throw new UnauthorizedException();

// Verify the JWT and check if it has been revoked
//--------------------------------------------------------------------------
const payload: JwtPayload = await this.jwtService.verifyAsync(
request.cookies['jwt'],
{ secret: process.env.JWT_SECRET },
);

if (
await this.prisma.revokedToken.findUnique({
where: { jti: payload.jti },
})
)
throw new UnauthorizedException();

// Attach user's data to the request
//--------------------------------------------------------------------------
request.user = payload;

return true;
} catch (err: unknown) {
throw new UnauthorizedException();
}
}
}

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.

Creating a user service

Let's create a user service to handle user-related tasks such as authentication, adding or fetching users from a database.

Open the file named src/user/user.service.ts and add the following code:

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { SignInDto } from './dto/SignIn.dto';
import { JwtService } from '@nestjs/jwt';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class UserService {
constructor(
private readonly jwtService: JwtService,
private readonly prisma: PrismaService,
) {}

async signUp(user: SignInDto) {
const newUser = await this.prisma.user.create({ data: user });

const token = await this.jwtService.signAsync(
{},
{ jwtid: uuidv4(), subject: newUser.id },
);

return { user: newUser, token };
}

async signIn(credentials: SignInDto) {
const user = await this.prisma.user.findUnique({
where: { email: credentials.email },
});

if (!user) throw new NotFoundException('User not found');

const token = await this.jwtService.signAsync(
{},
{ jwtid: uuidv4(), subject: user.id },
);

return { user, token };
}

async revokeToken(jti: string) {
await this.prisma.revokedToken.create({ data: { jti } });

return true;
}

async getById(id: string) {
return await this.prisma.user.findUniqueOrThrow({ where: { id } });
}
}

Sign up Method

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.

Sign in Method

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.

Revoke Token Method

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.

Get by ID Method

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.

Testing the authentication system

Finally, let's test our authentication system. Start your NestJS server by running the following command in your terminal:

yarn start

Then, send a POST request to http://localhost:3000/user/auth/sign-up with the following JSON payload:

{
"email": "[email protected]",
"password": "P@ssw0rd"
}

If everything is set up correctly, you should receive a response with a JWT token in the Cookie header:

{
"user": User
}

Removing the password from the response

Even though the password returned in the response is hashed, it is a good practice to remove it from the response.

info

💡 Nest’s interceptors are useful to transform response object at the end of the request-response cycle.

Run this command to generate src/interceptors/remove-password/remove-password.interceptor.ts:

nest g interceptor interceptors/removePassword

Open the file and modify it like that:

import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class RemovePasswordInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
delete data.password;
return data;
}),
);
}
}

Add @UseInterceptors(RemovePasswordInterceptor) before the routes you want to intercept.

caution

⚠️ Controllers in library specific mode (sign-up / sign-in routes) do not enable this.

If you retest your routes, password should have disappeared from responses.


Congratulations! You have successfully created a JWT-based authentication system with NestJS.

Results

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.

· 18 min read
Loubna Benzaama

caution

This tutorial is the continuation of the How to create a JWT-based authentication with NestJS tutorial. We recommend you to do it first before starting this one.

Find the full project in our Github repository.

Prerequisites

Before we begin, make sure that you have the following installed on your machine:

We will use MongoDB for our database. You can create a free instance on Mongo Atlas. Make sure to retrieve the connection URL from your dashboard.

Setting up your environment

Installing Nestjs CLI

To install Nestjs CLI, run:

yarn global add @nestjs/cli

Installing Dependencies

  • axios: A promise based HTTP client for Node.js
  • nodemailer: A package that allow easy email sending
yarn add axios nodemailer

Dev dependencies

  • @types/.*: provide the necessary typings for you to use libraries or modules in your TypeScript code with type checking and IntelliSense support

Update the schema.prisma file

In the last tutorial we have generated a schema.prisma file inside the prisma directory. Update it to add different collections:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}

model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
password String
publicAddress String @unique
items Item[] @relation()
sentTransfers Transfer[] @relation("sentTransfers")
receivedTransfers Transfer[] @relation("receivedTransfers")
createdAt DateTime @default(now())
}

model Collection {
id String @id @default(auto()) @map("_id") @db.ObjectId
contractAddress String @unique
name String @unique
description String
items Item[]
nextTokenId Int
}

model Item {
id String @id @default(auto()) @map("_id") @db.ObjectId
collection Collection @relation(fields: [collectionId], references: [id])
collectionId String @db.ObjectId
tokenId String @unique
ownerAddress String @unique
owner User? @relation(fields: [ownerId], references: [id])
ownerId String? @db.ObjectId
transfers Transfer[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Transfer {
id String @id @default(auto()) @map("_id") @db.ObjectId
item Item @relation(fields: [itemId], references: [id])
itemId String @db.ObjectId
from String
fromUser User @relation("sentTransfers", fields: [fromUserId], references: [id])
fromUserId String @db.ObjectId
to String
toUser User @relation("receivedTransfers", fields: [toUserId], references: [id])
toUserId String @db.ObjectId
txHash String @unique
createdAt DateTime @default(now())
}

model RevokedToken {
id String @id @default(auto()) @map("_id") @db.ObjectId
jti String @unique
}

User

The *User* collection is defined to store user information. It has the following fields:

  • id: The unique identifier for the user, generated automatically.
  • email: The user's email address, which must be unique.
  • password: The user's password.
  • publicAddress: The user's public address on the blockchain.
  • items: An array of Item objects that the user owns.
  • sentTransfers: An array of Transfer objects representing transfers that the user has sent.
  • receivedTransfers: An array of Transfer objects representing transfers that the user has received.
  • createdAt: The date and time the user was created, generated automatically.

Collection

The *Collection* collection is defined to store information about NFT collections. It has the following fields:

  • id: The unique identifier for the collection, generated automatically.
  • contractAddress: The public address of the contract that manages the collection on the blockchain.
  • name: The name of the collection, which must be unique.
  • description: A description of the collection.
  • items: An array of Item objects representing the NFTs in the collection.
  • nextTokenId: The ID of the next NFT to be created in the collection.

Item

The *Item* collection is defined to store information about NFTs. It has the following fields:

  • id: The unique identifier for the NFT, generated automatically.
  • collection: The collection that the NFT belongs to.
  • collectionId: The ID of the Collection that the NFT belongs to.
  • tokenId: The ID of the NFT on the blockchain.
  • ownerAddress: The public address of the current owner of the NFT on the blockchain.
  • owner: An array of User objects representing the users that own the NFT.
  • ownerId: An array of ids of the Users that own the NFT.
  • transfers: An array of Transfer objects representing the transfers that the NFT has been involved in.
  • createdAt: The date and time the NFT was created, generated automatically.
  • updatedAt: The date and time the NFT was last updated.

Transfer

The *Transfer* collection is defined to store information about transfers of NFTs. It has the following fields:

  • id: The unique identifier for the transfer, generated automatically.
  • item: The Item object representing the NFT that was transferred.
  • itemId: The id of the Item that was transferred.
  • from: The public address of the user who sent the NFT.
  • fromUser: The User object representing the user who sent the NFT.
  • fromUserId: The id of the User who sent the NFT.
  • to: The public address of the user who received the NFT.
  • toUser: The User object representing the user who received the NFT.
  • toUserId: The id of the User who received the NFT.
  • txHash: The transaction hash of the transfer on the blockchain.
  • createdAt: The date and time the transfer was created, generated automatically.

RevokedToken

The *RevokedToken* collection is defined to store information about revoked JWT tokens. It has the following fields:

  • id: The unique identifier for the revoked token, generated automatically.
  • jti: The "JWT ID" of the revoked token, which must be unique.

Updating the Sign-up route

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:

import { IsEmail, IsEthereumAddress, IsStrongPassword } from 'class-validator';

export default class SignUpDto {
@IsEmail()
email: string;

@IsStrongPassword()
password: string;

@IsEthereumAddress()
publicAddress: string;
}

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.

Update the DTO type in the controller:

@Post('auth/sign-up')
async signUp(
@Body() newUser: SignUpDto,
@Res() res: Response,
): Promise<void> {
try {
const { user, token }: { user: Partial<User>; token: string } =
await this.userService.signUp(newUser);

delete user.password;

res.cookie('jwt', token, { httpOnly: true });

res.status(201).send({ user: user });
} catch (err: unknown) {
throw new InternalServerErrorException(err);
}
}

Creating a Starton module

To simplify development, we will create a StartonModule. This module will wrap API calls into utility methods.

nest g module starton
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import {
cryptoquartzCollectionAddress,
network,
signerWallet,
} from 'src/utils/constants';

@Injectable()
export class StartonService {
private readonly instance = axios.create({
baseURL:
'https://api.starton.com/v3/smart-contract/' +
network +
'/' +
cryptoquartzCollectionAddress +
'/',
headers: {
'x-api-key': process.env.STARTON_API_KEY,
},
});

async initTransfer(from: string, to: string, tokenId: string) {
await this.instance.post('call', {
signerWallet,
functionName: 'safeTransferFrom',
params: [from, to, tokenId, 1, '0x00'],
});
}

async initMint(to: string, tokenId: string) {
await this.instance.post('call', {
signerWallet,
functionName: 'mint',
params: [to, tokenId, 1, '0x00'],
});
}

async initBurn(address: string, tokenId: string) {
await this.instance.post('call', {
signerWallet,
functionName: 'burn',
params: [address, tokenId, '0x00'],
});
}
}

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:

initTransfer

Initiate an NFT transfer by making a safeTransferFrom function call to the smart-contract

initMint

Initiate an NFT mint by making a mint function call to the smart-contract

initBurn

Initiate an NFT burn by making a burn function call to the smart-contract

Now that your have your StartonService, export it in a module:

nest g module starton
import { Module } from '@nestjs/common';
import { StartonService } from './starton.service';

@Module({
imports: [],
controllers: [],
providers: [StartonService],
exports: [StartonService],
})
export class StartonModule {}

Creating an EmailModule

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()
export class EmailService {
// Utility object to send emails
//--------------------------------------------------------------------------
private readonly _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
//--------------------------------------------------------------------------
async sendEmail(to: string, subject: string, text: string) {
await this._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.

Create an Item resource

ItemService

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()
export class ItemService {
constructor(private readonly prisma: PrismaService) {}

async create(data: Prisma.ItemCreateInput) {
await this.prisma.item.create({ data });
}

async updateByTokenId(tokenId: string, data: Prisma.ItemUpdateInput) {
await this.prisma.item.update({ where: { tokenId }, data });
}

async deleteByTokenId(tokenId: string) {
await this.prisma.item.delete({ where: { tokenId } });
}

async safeTransferFrom(
collection: string,
tokenId: string,
from: string,
to: string,
) {
// Add item to database in case of a mint (from === nullAddress)
//--------------------------------------------------------------------------
if (from === nullAddress) {
await this.prisma.item.create({
data: {
collection: {
connect: {
contractAddress: collection,
},
},
tokenId,
ownerAddress: to,
},
});

// Increment nextTokenId in the database
//--------------------------------------------------------------------------
await this.prisma.collection.update({
where: {
contractAddress: collection,
},
data: {
nextTokenId: {
increment: 1,
},
},
});

// Update item's owner in case of a transfer
//--------------------------------------------------------------------------
} else {
await this.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.

ItemModule

Now that we have an Item service we need to export it in a module to be able to use it elsewhere.

/*
| Developed by Starton
| Filename : item.module.ts
| Author : Alexandre Schaffner ([email protected])
*/

import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/prisma/prisma.module';

import { ItemService } from './item.service';

/*
|--------------------------------------------------------------------------
| ITEM MODULE
|--------------------------------------------------------------------------
*/
@Module({
imports: [PrismaModule],
providers: [ItemService],
exports: [ItemService],
})
export class ItemModule {}

Create a Transfer resource

TransferService

/*
| 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()
export class TransferService {
constructor(private readonly prisma: PrismaService) {}

// Create a transfer record in the database
//--------------------------------------------------------------------------
async create(transfer: Prisma.TransferCreateInput) {
await this.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.

TransferController

First things first, let’s generate the TransferController:

nest g controller transfer

Watcher & Webhook

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')
export class TransferController {
constructor(
private readonly transferService: TransferService,
private readonly starton: StartonService,
private readonly userService: UserService,
private readonly itemService: ItemService,
private readonly emailService: EmailService,
) {}

/*
|--------------------------------------------------------------------------
| WEBHOOK ENDPOINT TRIGGERED BY STARTON
|--------------------------------------------------------------------------
*/

@UseGuards(StartonGuard)
@Post('webhook')
async webhook(@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 = await this.userService.findByPublicAddress(
to.toLowerCase(),
);
if (!toUser) delete transfer.toUser;
const fromUser = await this.userService.findByPublicAddress(
from.toLowerCase(),
);
if (!fromUser) delete transfer.fromUser;

// Change the owner of the item in the database
//--------------------------------------------------------------------------
await this.itemService.safeTransferFrom(
cryptoquartzCollectionAddress,
id.hex.toLowerCase(),
from.toLowerCase(),
to.toLowerCase(),
);

// Create the transfer record
//--------------------------------------------------------------------------
await this.transferService.create(transfer);

// If the recipient is a user, send an email
//--------------------------------------------------------------------------
if (!toUser) return;

// Use a template here
//--------------------------------------------------------------------------
await this.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);
throw new InternalServerErrorException();
}
}
}

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.

StartonGuard

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()
export class StartonGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request: Request = context.switchToHttp().getRequest();
const payload = JSON.stringify(request.body);

const reqSignature = request.get('starton-signature');
if (!reqSignature) return false;

// Re-compute the signature and compare it with the one received
//--------------------------------------------------------------------------
const localSignature = createHmac(
'sha256',
process.env.STARTON_SECRET as string,
)
.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.

Utility endpoints

Next, we add endpoints to allow minting, burning and transfers of items.

/*
|--------------------------------------------------------------------------
| TRANSFER / MINT / BURN ENDPOINTS
|--------------------------------------------------------------------------
*/

// safeTransferFrom
//--------------------------------------------------------------------------
@UseGuards(AuthGuard)
@Post()
async safeTransferFrom(@Body() safeTransferDto: SafeTransferDto) {
await this.starton.initTransfer(
safeTransferDto.from,
safeTransferDto.to,
safeTransferDto.tokenId,
);
}

// Mint a token
//--------------------------------------------------------------------------
@UseGuards(AuthGuard)
@Post('mint')
async mint(@Body() mintDto: MintDto) {
await this.starton.initMint(mintDto.to, mintDto.tokenId);
}

// Burn a token
//--------------------------------------------------------------------------
@UseGuards(AuthGuard)
@Post('burn')
async burn(@Body() burnDto: MintDto) {
await this.starton.initBurn(burnDto.to, burnDto.tokenId);
await this.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.

TransferModule

Finally, we group TransferService and TransferController in a TransferModule:

nest g module transfer
import { Module } from '@nestjs/common';
import { EmailModule } from 'src/email/email.module';
import { ItemModule } from 'src/item/item.module';
import { PrismaModule } from 'src/prisma/prisma.module';
import { StartonModule } from 'src/starton/starton.module';
import { UserModule } from 'src/user/user.module';

import { TransferService } from './transfer.service';

@Module({
imports: [PrismaModule, StartonModule, UserModule, ItemModule, EmailModule],
providers: [TransferService],
})
export class TransferModule {}

Now that everything looks fine you can test your code by running

yarn start

Congratulations!

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.

· 5 min read
Loubna Benzaama

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:

Step 1 - Initialize the project

  1. Start by creating a directory for your project.

    mkdir monitor-blockchain
  2. Then, get into your project directory.

    cd monitor-blockchain
  3. With your project directory set up, install the dependencies.

    npm add axios

This library will allow you to make request to the Starton API. For more information on axios, check out Axios documentation.

  1. Then, usetouch to create a index.js file and open it with your favorite editor.

    touch list.js

Step 2 - Add Starton to your project

  1. First, import axios.

    const axios = require("axios")
  2. Then, initialize axios using Starton URL and authenticate with your API KEY.

    Create an API key


    const starton = axios.create({
    baseURL: "https://api.starton.com",
    headers: {
    "x-api-key": "YOUR API KEY",
    },
    })

Step 3 - Creating a watcher

What is 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.

  1. 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.
  2. 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!


Check all triggered events on Starton Web Application



Congratulations on creating your first watcher! In this tutorial, you discovered how to monitor blockchain activity but this is only the first step.

What's next?

Learn how to:

· 2 min read
Loubna Benzaama

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.

Installing ngrok

  1. Go to Ngrok.
  2. Install ngrok on your system.

Launching your ngrok URL

  1. Launch your web application.
  2. Go to your localhost port. For example, http://localhost:3001.
  3. Launch ngrok with the command ngrok http 3001.
  4. Go to your Ngrok Web Interface http://127.0.0.1:4040.
  5. Click the Status tab.
  6. Copy the URL of the tunnel which should look like this https://xxxxxxxx.ngrok.io.

Creating your watcher in Starton

You can use this Webhook URL in Starton to create your watcher.

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

· 3 min read
Loubna Benzaama

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.

You will need:

Create a webhook

  1. From Starton, go to Tools.
  2. Click on the Zapier card.
  3. Log in with your Zapier credentials.
  4. Click Accept Invite & Build a Zap.
  5. Click Create Zap.
  6. In Trigger, select Webhook.
  7. In Event, select Catch Raw Hook and click to Continue.

Zapier provides you with a Webhook URL. We will need this URL in our Watcher on Starton.

Creating a watcher in Starton

  1. In Starton, go to Monitor.
  2. Click + Watcher.
  3. 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.
  4. Enter the watcher's Name and Description. Click Next.
  5. Select the blockchain on which you want to track the wallet (for example, we use Binance Smart Chain testnet ).
  6. Enter the Wallet Address that you need to track.
  7. Copy the URL and paste in Webhook URL.
  8. 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.
  9. Click Create.

Testing the trigger

To test, the webhook and the watcher, we need to send currency to the wallet to trigger for an activity event.

  1. Transfer tokens to the wallet you need to watch. This will trigger an event in your watcher.
  2. Wait for the number of Confirmations Block configured.
  3. Come back to Zapier and click Test trigger. If it's successful, you will read We found a request!.
  4. Click Continue.

Configure the googlesheets

  1. In Zapier, select an Action.
    1. For this integration, select Google Sheets.
    2. In Event, select Create Spreadsheet Row and click Continue.
    3. Link your Google Sheets account.
    4. Select your Google Sheets account.
    5. Click to Continue.
  2. In Google Drive, go to your Google Sheets.
  3. Create a new Google sheets.
  4. In Zapier, enter your spreadsheet.
    1. Select a Worksheet, for this example we use Sheet1.
    2. On your Google sheets, create a header in cell A1, and type Your event name, for this example ADDRESS_ACTIVITY.
    3. On Zapier, you can view the Event name typed in Your event name in the Google Sheets.
    4. Select the data you want to save in your google sheets.
    5. Click Continue.
  5. Click to Test action. If successful, you can see your data in your google sheets.
  6. Now you can save your zap.

Congratulations! The activity of your wallet is now tracked on a Googlesheets.