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
10 changes: 10 additions & 0 deletions src/controllers/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Request, Response } from 'express';
import httpStatus from 'http-status';

import { CollectionService } from '../services';
import { assert } from '../utils';

class CollectionController {
async searchCollections(req: Request, res: Response): Promise<void> {
Expand Down Expand Up @@ -33,6 +34,15 @@ class CollectionController {
data: collectionWithTokens,
});
}

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

assert(collection != null, 'Collection id does not exists');
assert(collection?.issuer == req.user?.address, 'Only issuer of collection can verify collection');
}
}

export default new CollectionController();
2 changes: 2 additions & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { default as CollectionController } from './collection';
export { default as ListingController } from './listing';
export { default as TokenController } from './token';
export { default as UserController } from './user';
export { default as OfferController } from './offer';
50 changes: 8 additions & 42 deletions src/controllers/listing.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Listing } from '@prisma/client';
import { Request, Response } from 'express';
import httpStatus from 'http-status';
import { NFTokenCancelOffer, NFTokenCreateOffer } from 'xrpl';

import { configuration } from '../config';
import { ListingDBFilters } from '../interfaces';
import { ListingService, TokenService } from '../services';
import { XrplClient, assert, pick } from '../utils';
import { ListingService } from '../services';
import { XrplClient, assert, pick, NFTBroker } from '../utils';

class ListingController {
async getListingById(req: Request, res: Response): Promise<void> {
Expand All @@ -29,44 +27,12 @@ class ListingController {
}

async createListing(req: Request, res: Response): Promise<void> {
const { txHash } = req.body;
const tx = (await XrplClient.getTransaction(txHash)) as NFTokenCreateOffer;

assert(tx.Account == req.user?.address, 'Not authorised');
assert(tx.Flags == 1, 'Offer type in transaction is not of right type');
assert(tx.TransactionType == 'NFTokenCreateOffer', "Transaction provided is of wrong type'");
assert(
tx.Destination == XrplClient.getAddressFromPrivateKey(configuration.XRPL_ACCOUNT_SECRET),
'Transaction destination address does not belong to Optimart',
);

const ongoingListings = await ListingService.all({ status: 'ONGOING' });
assert(
ongoingListings.length == 0,
'A listing for this token is still ongoing, cancel/close it before creating a new one',
);
const { txHash, extraPayload } = req.body;
const { type } = extraPayload;

const nft = await TokenService.getOrCreateByTokenId(tx.NFTokenID);
const listing = await ListingService.create({
creator: {
connectOrCreate: {
where: {
address: tx.Account,
},
create: {
address: tx.Account,
},
},
},
price: Number(tx.Amount),
nft: {
connect: {
tokenId: nft.tokenId,
},
},
createTxnHash: txHash,
});
const listing = await NFTBroker.createListing(txHash, type, req.user?.address);
res.status(httpStatus.OK).json({ data: listing });

// todo: consider updating floor price of collection depending on listing price
}

Expand All @@ -79,12 +45,12 @@ class ListingController {
assert(listing?.creatorAddr == req.user?.address, 'Listing creator mismatch with authenticated user');
assert(listing?.status == 'ONGOING', 'Listing must be currently ongoing');

const txn = (await XrplClient.getTransaction(txHash)) as NFTokenCancelOffer;
const txn = (await XrplClient.getTransaction(txHash)).result;

assert(txn.TransactionType == 'NFTokenCancelOffer', "Transaction provided is of wrong type'");
assert(txn.Account == req.user?.address, 'Not authorised');

await ListingService.cancelListing(listingId, txHash);
await NFTBroker.cancelListing(listingId, txHash);
res.status(httpStatus.NO_CONTENT).json();
}
}
Expand Down
168 changes: 168 additions & 0 deletions src/controllers/offer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { ListingOffer } from '@prisma/client';
import { Request, Response } from 'express';
import httpStatus from 'http-status';
import { TransactionMetadata, rippleTimeToUnixTime } from 'xrpl';

import { configuration } from '../config';
import { NFTCreateOfferWithId, OfferDBFilters } from '../interfaces';
import { ListingOfferService, ListingService, TokenService } from '../services';
import { NFTBroker, XrplClient, assert, pick } from '../utils';

class OfferController {
async getOfferById(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const offer = await ListingOfferService.getById(id);
res.status(httpStatus.OK).json({ data: offer });
}

async getOffers(req: Request, res: Response): Promise<void> {
const filters = pick(req.query, [
'status',
'limit',
'offset',
'offeree',
'offeror',
'listing',
'nftId',
'offeredBefore',
'offeredAfter',
]) as OfferDBFilters;

const offers: ListingOffer[] = await ListingOfferService.all(filters);
const offersCount = await ListingOfferService.count(filters);
res.status(httpStatus.OK).json({
data: {
total: offersCount,
offers: offers,
},
});
}

async createOffers(req: Request, res: Response): Promise<void> {
const { txHash, extraPayload } = req.body;
const { listingId } = extraPayload;
const tx = (await XrplClient.getTransaction(txHash)).result;
const nftOffer = XrplClient.parseNFTCreateOfferFromTxnMetadata(
tx.meta as TransactionMetadata,
) as NFTCreateOfferWithId;
const endAt = nftOffer.Expiration ? rippleTimeToUnixTime(nftOffer.Expiration) : undefined;
if (endAt) {
assert(endAt > Date.now(), 'Invalid value for ending timestamp');
}
assert(tx.Account == req.user?.address, 'Not authorised');
assert(nftOffer.Flags != 1, 'Offer type in transaction is not of right type');
assert(tx.TransactionType == 'NFTokenCreateOffer', "Transaction provided is of wrong type'");
assert(
nftOffer.Destination == XrplClient.getAddressFromPrivateKey(configuration.XRPL_ACCOUNT_SECRET),
'Transaction destination address does not belong to Optimart',
);

const nft = await TokenService.getOrCreateByTokenId(nftOffer.NFTokenID);
let listing;
if (listingId) {
listing = await ListingService.getById(listingId);
assert(listing != null && listing.status == 'ONGOING', 'Invalid listing id provided');
}

const offer = await ListingOfferService.create({
createTxnHash: txHash,
amount: Number(nftOffer.Amount),
nft: {
connect: {
tokenId: nft.tokenId,
},
},
offeree: {
connectOrCreate: {
where: {
address: nftOffer.Owner,
},
create: {
address: nftOffer.Owner as string,
},
},
},
endAt: endAt ? new Date(endAt) : undefined,
listing: {
connect: { id: listingId },
},
offeror: {
connectOrCreate: {
where: {
address: tx.Account,
},
create: {
address: tx.Account as string,
},
},
},
buyOfferId: nftOffer.id,
});

if (listing && listing.price.toNumber() <= offer.amount.toNumber()) {
await NFTBroker.acceptListingOffer(offer.id);
}
res.status(httpStatus.OK).json({ data: offer });
}

async cancelOffer(req: Request, res: Response): Promise<void> {
const { txHash, extraPayload } = req.body;
const { offerId } = extraPayload;
const offer = await ListingOfferService.getById(offerId);

assert(offer != null, 'Offer not found in database');
assert(offer?.status == 'PENDING', 'Offer must be currently pending');
assert(offer?.offerorAddr == req.user?.address, 'Offers can only be cancelled by creator');

const txn = (await XrplClient.getTransaction(txHash)).result;

assert(txn.TransactionType == 'NFTokenCancelOffer', "Transaction provided is of wrong type'");
assert(txn.Account == req.user?.address, 'Not authorised');
await ListingOfferService.cancel(offerId, txHash);
res.status(httpStatus.NO_CONTENT).json();
}

async rejectOffer(req: Request, res: Response): Promise<void> {
const { txHash, extraPayload } = req.body;
const { offerId } = extraPayload;
const offer = await ListingOfferService.getById(offerId);

assert(offer != null, 'Offer not found in database');
assert(offer?.status == 'PENDING', 'Offer must be currently pending');
assert(offer?.offereeAddr == req.user?.address, 'Offers can only be rejected by receiver');

const txn = (await XrplClient.getTransaction(txHash)).result;

assert(txn.TransactionType == 'NFTokenCancelOffer', "Transaction provided is of wrong type'");
assert(txn.Account == req.user?.address, 'Not authorised');

await ListingOfferService.reject(offerId, txHash);
res.status(httpStatus.NO_CONTENT).json();
}

async acceptOffer(req: Request, res: Response): Promise<void> {
const { txHash, extraPayload } = req.body;
const { offerId } = extraPayload;
const offer = await ListingOfferService.getById(offerId);

assert(offer != null, 'Offer not found in database');
assert(offer?.status == 'PENDING', 'Offer must be currently pending');
assert(offer?.offereeAddr == req.user?.address, 'Offers can only be rejected by receiver');

if (!offer?.listingId) {
assert(txHash != undefined, '');
const listing = await NFTBroker.createListing(txHash, 'REGULAR', req.user?.address);
await ListingOfferService.update(offerId, {
listing: {
connect: {
id: listing.id,
},
},
});
}
await NFTBroker.acceptListingOffer(offerId);
res.status(httpStatus.NO_CONTENT).json();
}
}

export default new OfferController();
23 changes: 23 additions & 0 deletions src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Request, Response } from 'express';
import httpStatus from 'http-status';

import { UserService } from '../services';
import { XrplClient } from '../utils';

class UserController {
async getByAddr(req: Request, res: Response): Promise<void> {
const { addr } = req.params;
const user = await UserService.getOrCreateByAddr(addr);
const balance = await XrplClient.getAccountInfo(addr);
const nfts = await XrplClient.getAccountNFTInfo(addr);
res.status(httpStatus.OK).json({
data: {
...user,
xrpBalance: balance,
nfts: nfts,
},
});
}
}

export default new UserController();
File renamed without changes.
21 changes: 19 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Nft } from '@prisma/client';
import { Transaction, TransactionMetadata } from 'xrpl';
import { NFTokenCreateOffer, Transaction, TransactionMetadata } from 'xrpl';
import { LedgerIndex, NFTOffer } from 'xrpl/dist/npm/models/common';
import { BaseResponse } from 'xrpl/dist/npm/models/methods/baseMethod';

Expand All @@ -14,6 +14,9 @@ interface NFTMetadataAttribute {
name: string;
value: string;
}
export interface NFTCreateOfferWithId extends NFTokenCreateOffer {
id: string;
}

export interface NFTHistoryTxnsResponse extends BaseResponse {
result: {
Expand Down Expand Up @@ -41,7 +44,7 @@ export interface NFTInfoResponse extends BaseResponse {
}

export interface CollectionTokensData {
name: string;
name: string | null;
nfts: Nft[];
}

Expand All @@ -60,4 +63,18 @@ export interface ListingDBFilters {
creator?: string;
listedBefore?: Date;
listedAfter?: Date;
nftId?: string;
type?: 'REGULAR' | 'AUCTION';
}

export interface OfferDBFilters {
limit?: number;
offset?: number;
offeree?: string;
offeror?: string;
listing?: string;
offeredBefore?: Date;
offeredAfter?: Date;
nftId?: string;
status?: 'PENDING' | 'CANCELLED' | 'ACCEPTED' | 'REJECTED';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Warnings:

- Added the required column `duration` to the `Listing` table without a default value. This is not possible if the table is not empty.
- Added the required column `nftId` to the `ListingOffer` table without a default value. This is not possible if the table is not empty.

*/
-- CreateEnum
CREATE TYPE "ListingType" AS ENUM ('REGULAR', 'AUCTION');

-- DropForeignKey
ALTER TABLE "Offer" DROP CONSTRAINT "Offer_nftId_fkey";

-- DropIndex
DROP INDEX "Auction_nftId_key";

-- DropIndex
DROP INDEX "Listing_nftId_key";

-- AlterTable
ALTER TABLE "Listing" ADD COLUMN "duration" DECIMAL(65,30) NOT NULL;

-- AlterTable
ALTER TABLE "ListingOffer" ADD COLUMN "nftId" TEXT NOT NULL,
ADD COLUMN "type" "ListingType" NOT NULL DEFAULT 'REGULAR',
ALTER COLUMN "listingId" DROP NOT NULL;

-- AddForeignKey
ALTER TABLE "ListingOffer" ADD CONSTRAINT "ListingOffer_nftId_fkey" FOREIGN KEY ("nftId") REFERENCES "Nft"("tokenId") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Listing" ALTER COLUMN "duration" DROP NOT NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- You are about to drop the column `type` on the `ListingOffer` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "Listing" ADD COLUMN "type" "ListingType" NOT NULL DEFAULT 'REGULAR';

-- AlterTable
ALTER TABLE "ListingOffer" DROP COLUMN "type";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ListingOffer" ADD COLUMN "duration" DECIMAL(65,30);
Loading