Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
478 changes: 478 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/server.ts",
"lint": "eslint --cache .",
"format": "eslint --fix . && prettier --write .",
"db:seed": "npx prisma db seed -- -p <path_to_directory>",
"db:seed": "npx prisma db seed",
"swagger:gendoc": "ts-node src/docs/swagger.ts",
"db:migrate:dev": "npx prisma migrate dev --skip-seed",
"db:migrate:deploy": "npx prisma migrate deploy",
Expand All @@ -33,6 +33,8 @@
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.0.0",
"google-auth-library": "^8.9.0",
"google-spreadsheet": "^4.0.2",
"jest": "^29.5.0",
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
Expand Down
5 changes: 4 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import helmet from 'helmet';
import httpStatus from 'http-status';
import swaggerUi from 'swagger-ui-express';

import collectionRouter from './collections/collections.router';
import swaggerDocument from './docs/swagger.json';
import listingRouter from './listings/listings.router';
import { morgan, errorHandler, errorConverter } from './middlewares';
import { pingRouter, collectionRouter, listingRouter, tokenRouter } from './routes';
import tokenRouter from './nfts/nfts.router';
import pingRouter from './ping/ping.router';
import ApiError from './utils/api-error';

const application: Application = express();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Request, Response } from 'express';
import httpStatus from 'http-status';

import { CollectionService } from '../services';
import CollectionService from './collections.service';

class CollectionController {
async searchCollections(req: Request, res: Response): Promise<void> {
const { q, limit } = req.query;
const matchingCollections = await CollectionService.search(q as string, Number(limit));

res.status(httpStatus.OK).json({
data: matchingCollections,
});
}

async getByCollectionId(req: Request, res: Response): Promise<void> {
async getCollectionById(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const collection = await CollectionService.getById(id as string);

Expand All @@ -21,14 +22,16 @@ class CollectionController {
});
}

async getTokensInCollection(req: Request, res: Response): Promise<void> {
// todo: Add filters
async getNFTsInCollection(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const { offset, limit } = req.query;
const collectionWithTokens = await CollectionService.getTokens(
id as string,
Number(limit),
Number(offset),
);

res.status(httpStatus.OK).json({
data: collectionWithTokens,
});
Expand Down
26 changes: 26 additions & 0 deletions src/collections/collections.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Router } from 'express';

import CollectionController from './collections.controller';
import CollectionValidation from './collections.validator';
import { validate } from '../middlewares';
import { catchAsync } from '../utils';

const collectionRouter = Router();

collectionRouter.get(
'/search',
validate(CollectionValidation.searchCollection),
catchAsync(CollectionController.searchCollections),
);
collectionRouter.get(
'/nfts/:id',
validate(CollectionValidation.getCollectionTokens),
catchAsync(CollectionController.getNFTsInCollection),
);
collectionRouter.get(
'/:id',
validate(CollectionValidation.getCollection),
catchAsync(CollectionController.getCollectionById),
);

export default collectionRouter;
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,10 @@ class CollectionService {
});
}

async all(limit: number, offset: number): Promise<Collection[]> {
return this.model.findMany({
skip: offset,
take: limit,
include: {
_count: {
select: { nfts: true },
},
},
});
}

async getById(id: string): Promise<Collection | null> {
return this.model.findUniqueOrThrow({
where: {
id: id,
collectionId: id,
},
include: {
_count: {
Expand All @@ -51,7 +39,7 @@ class CollectionService {
async getTokens(id: string, limit: number, offset: number): Promise<CollectionTokensData | null> {
return this.model.findFirst({
where: {
id: id,
collectionId: id,
},
select: {
name: true,
Expand Down
File renamed without changes.
3 changes: 0 additions & 3 deletions src/controllers/index.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/controllers/ping.ts

This file was deleted.

23 changes: 23 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,26 @@ export interface ListingDBFilters {
listedBefore?: Date;
listedAfter?: Date;
}

export interface TopCollectionsFilter {
limit?: number;
offset?: number;
duration: '24h' | '7d' | '30d' | 'allTime';
}

enum NftStatus {
UNLIST = 'UNLIST',
LIST = 'LIST',
AUCTION = 'AUCTION',
}

export interface NftFilter {
name?: string;
issuer?: string;
status?: NftStatus;
minPrice?: number;
maxPrice?: number;
taxon?: number;
attributesCount?: number;
attributes: Record<string, any>;
}
File renamed without changes.
4 changes: 2 additions & 2 deletions src/routes/listing.ts → src/listings/listings.router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Router } from 'express';

import { ListingController } from '../controllers';
import ListingController from './listings.controller';
import listingValidation from './listings.validator';
import { validate, authenticateSignature } from '../middlewares';
import { catchAsync } from '../utils';
import listingValidation from '../validators/listing';

const listingRouter = Router();

Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/controllers/token.ts → src/nfts/nfts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { parseNFTokenID } from 'xrpl';
import { TokenService } from '../services';
import { XrplClient } from '../utils';

class TokenController {
class NFTController {
async getTokenById(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const nftData = parseNFTokenID(id);
Expand All @@ -22,4 +22,4 @@ class TokenController {
}
}

export default new TokenController();
export default new NFTController();
12 changes: 12 additions & 0 deletions src/nfts/nfts.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Router } from 'express';

import NFTController from './nfts.controller';
import tokenValidation from './nfts.validator';
import { validate } from '../middlewares';
import { catchAsync } from '../utils';

const nftRouter = Router();

nftRouter.get('/:id', validate(tokenValidation.getById), catchAsync(NFTController.getTokenById));

export default nftRouter;
124 changes: 124 additions & 0 deletions src/nfts/nfts.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Nft, Prisma } from '@prisma/client';

import { NftFilter } from '../interfaces';
import prisma from '../prisma/index';
import { XrplClient } from '../utils';
import NFTMetadataService from '../utils/nft-metadata';

class NFTService {
model = prisma.nft;

async count(): Promise<number> {
return this.model.count();
}

async getById(id: string): Promise<Nft | null> {
return this.model.findUnique({
where: {
id: id,
},
});
}

getByTokenId(id: string): Promise<Nft | null> {
return this.model.findUnique({
where: {
tokenId: id,
},
});
}

async filter(filters: NftFilter): Promise<Nft[] | null> {
const filterConditions: Prisma.NftWhereInput[] = [];

if (filters.name || filters.taxon || filters.issuer) {
const collectionConditions: Prisma.CollectionWhereInput = {};

if (filters.name) {
collectionConditions.name = { contains: filters.name, mode: 'insensitive' };
}
if (filters.taxon) {
collectionConditions.taxon = filters.taxon;
}
if (filters.issuer) {
collectionConditions.issuer = filters.issuer;
}

filterConditions.push({ collection: collectionConditions });
}

if (filters.status) {
filterConditions.push({ status: filters.status });
}

if (filters.minPrice || filters.maxPrice) {
const priceConditions: Prisma.DecimalFilter = {};

if (filters.minPrice) {
priceConditions.gte = filters.minPrice;
}
if (filters.maxPrice) {
priceConditions.lte = filters.maxPrice;
}

filterConditions.push({ price: priceConditions });
}

if (filters.attributes) {
for (const [key, value] of Object.entries(filters.attributes)) {
filterConditions.push({
attributes: {
path: [key],
equals: value,
},
});
}
}

if (filters.attributesCount) {
const result: { id: string }[] =
await prisma.$queryRaw`SELECT "id" FROM "Nft" WHERE jsonb_object_keys(attributes)::jsonb ?& array[${filters.attributesCount}]`;
const idsWithCorrectAttributeCount = result.map((item) => item.id);

filterConditions.push({
id: {
in: idsWithCorrectAttributeCount,
},
});
}

const where = { AND: filterConditions };
return prisma.nft.findMany({ where });
}

async createByTokenId(id: string): Promise<Nft> {
const nftData = await XrplClient.getNFTInfo(id);
const metadata = await NFTMetadataService.resolveNFTMetadata(id, nftData.uri, nftData.issuer);
return this.create({
tokenId: id,
owner: nftData.owner,
sequence: nftData.nft_sequence,
attributes: JSON.stringify(metadata.attributes),
uri: nftData.uri,
imageUrl: metadata.imageUrl,
});
}

async create(data: Prisma.NftCreateInput): Promise<Nft> {
return this.model.create({ data: data });
}

async getOrCreateByTokenId(id: string): Promise<Nft> {
const token = await this.getByTokenId(id);
if (!token) {
return this.createByTokenId(id);
}
return token;
}

async update(id: string, data: Prisma.NftUpdateInput): Promise<Nft> {
return this.model.update({ where: { id: id }, data: data });
}
}

export default new NFTService();
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 10 additions & 0 deletions src/ping/ping.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Request, Response } from 'express';
import httpStatus from 'http-status';

class PingController {
async get(req: Request, res: Response): Promise<void> {
res.status(httpStatus.OK).json();
}
}

export default new PingController();
4 changes: 2 additions & 2 deletions src/routes/ping.ts → src/ping/ping.router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Router } from 'express';

import pingController from '../controllers/ping';
import PingController from './ping.controller';

const pingRouter = Router();

pingRouter.get('/ping', pingController);
pingRouter.get('/ping', PingController.get);

export default pingRouter;
36 changes: 0 additions & 36 deletions src/prisma/migrations/20230722144712_enum_add/migration.sql

This file was deleted.

Loading