diff --git a/lib/api/api.ts b/lib/api/api.ts index 72bb21e..08828b4 100644 --- a/lib/api/api.ts +++ b/lib/api/api.ts @@ -1,32 +1,15 @@ import { Marketplace } from "../marketplace"; -import express from 'express' -import bodyParser from 'body-parser' -import { OrderFront } from "../types/common"; -import { graphqlHTTP } from "express-graphql"; import { schema } from "./schema"; - +// import { ApolloServer} from "apollo-server"; +import express from "express"; +import { ApolloServer } from 'apollo-server-express'; +const {graphqlUploadExpress} = require("graphql-upload"); export async function start(marketplace: Marketplace) { - - const app = express() - const jsonParser = bodyParser.json() - - app.use( - "/graphql", - graphqlHTTP({ - schema: schema(marketplace), - graphiql: true, - })); - - app.post('/orders', jsonParser, async (req: any, res: any) => res.json(await marketplace.createOrder(OrderFront.fromJson(req.body)))); - app.get('/orders', async (req: any, res: any) => res.json(await marketplace.getOrders())); - app.get('/orders/:orderId', async (req: any, res: any) => res.json(await marketplace.getOrder(req.params.orderId))); - app.get('/tokens', async (req: any, res: any) => res.json(await marketplace.getTokens())); - app.post('/asset/create', async (req: any, res: any) => res.set('Status Code', 202)); - - - console.log("starting") - app.listen(8080); - + const app = express(); + app.use(graphqlUploadExpress()); + const server = new ApolloServer({schema: schema(marketplace)}); + await server.start(); + server.applyMiddleware({ app }); + await new Promise(resolve => app.listen({ port: 8080 }, resolve)); } - diff --git a/lib/api/schema.ts b/lib/api/schema.ts index 1857f67..572a4dd 100644 --- a/lib/api/schema.ts +++ b/lib/api/schema.ts @@ -9,12 +9,45 @@ import { GraphQLString } from "graphql"; -import {Order, TokensCollection} from "../types/mongo"; +import {Order, TokensCollection, Tokens} from "../types/mongo"; import {OrderFront} from "../types/common"; import {Marketplace} from "../marketplace"; +const {GraphQLUpload} = require("graphql-upload"); + +// import {util} from "prettier"; +// import skip = util.skip; + export function schema(marketplace: Marketplace): GraphQLSchema { + // const FileType = new GraphQLObjectType({ + // name: "FileType", + // fields: () => ({ + // filename: {type: GraphQLString}, + // mimetype: {type: GraphQLString}, + // encoding: {type: GraphQLString} + // }) + // }); + + + const PageInfoType = new GraphQLObjectType({ + name: "PageInfo", + fields: () => ({ + hasNext: {type: GraphQLBoolean}, + nextCursor: {type: GraphQLString} + }) + }); + + + const Page = (itemType: any, pageName: any) => { + return new GraphQLObjectType({ + name: pageName, + fields: () => ({ + results: {type: new GraphQLList(itemType)}, + pageInfo: {type: PageInfoType} + }) + }); + } const TokenOwnersType = new GraphQLScalarType({ @@ -37,13 +70,27 @@ export function schema(marketplace: Marketplace): GraphQLSchema { }) }); + const EventType = new GraphQLObjectType({ + name: "Event", + fields: () => ({ + from: {type: GraphQLString}, + to: {type: GraphQLString}, + quantity: {type: GraphQLInt}, + timestamp: {type: GraphQLInt}, + txHash: {type: GraphQLString}, + }) + }); const TokenType = new GraphQLObjectType({ name: "Token", fields: () => ({ + collectionObjectId: {type: GraphQLString}, tokenId: {type: GraphQLString}, + metadata_uri: {type: GraphQLString}, metadata: {type: MetadataType}, + last_update: {type: GraphQLInt}, owners: {type: TokenOwnersType}, + events: {type: new GraphQLList(EventType)} }) }); @@ -54,7 +101,6 @@ export function schema(marketplace: Marketplace): GraphQLSchema { contractAddress: {type: GraphQLString}, tokenType: {type: GraphQLInt}, owner: {type: GraphQLString}, - tokens: {type: new GraphQLList(TokenType),} }) }); @@ -86,26 +132,78 @@ export function schema(marketplace: Marketplace): GraphQLSchema { fields: () => ({ tokensCollection: { type: new GraphQLList(TokensCollectionType), - resolve: () => { + resolve: async () => { return TokensCollection - .find({tokenType: {$ne: null}}) - .sort({'tokens.last_update': 1}); + .find({tokenType: {$ne: null}}) + .sort({'tokens.last_update': 1}); + } + }, + + getTokens: { + type: Page(TokenType, "AllTokensPage"), + args: { + first: {type: GraphQLInt}, + cursor: {type: GraphQLString } + }, + resolve: async (_, args) => { + args.cursor = args.cursor === null ? undefined : args.cursor; + + // @ts-ignore + return Tokens + .find({}) + .sort({last_update: -1}) + .limit(args.first) + .paginate(args.cursor) // noinspection } }, + getTokensByOwner: { - type: new GraphQLList(TokensCollectionType), - args: {owner: {type: GraphQLString}}, - resolve: (_, args) => { - return TokensCollection.find({ - tokenType: {$ne: null}, - tokens: {$elemMatch: {[`owners.${args.owner}`]: {$gt: 0}}} - }) + type: Page(TokenType, "TokensByOwnerPage"), + args: { + owner: {type: GraphQLString}, + first: {type: GraphQLInt}, + cursor: {type: GraphQLString } + }, + resolve: async (_, args) => { + args.cursor = args.cursor === null ? undefined : args.cursor; + + // @ts-ignore + return Tokens + .find({[`owners.${args.owner}`]: {$gt: 0}}) + .sort({last_update: -1}) + .limit(args.first) + .paginate(args.cursor) // noinspection + } + }, + + getSpecificToken: { + type: Page(TokenType, "SpecificTokens"), + args: { + tokenId: {type: GraphQLString}, + metadataName: {type: GraphQLString}, + owner: {type: GraphQLString}, + first: {type: GraphQLInt}, + cursor: {type: GraphQLString } + }, + resolve: async (_, args) => { + args.cursor = args.cursor === null ? undefined : args.cursor; + + // @ts-ignore + return Tokens + .find({$or: [ + {[`owners.${args.owner}`]: {$gt: 0}}, + {"metadata.name": args.metadataName}, + {"tokenId": args.tokendId}, + ]}) + .sort({last_update: -1}) + .limit(args.first) + .paginate(args.cursor) // noinspection } }, orders: { type: new GraphQLList(OrderType), - resolve: () => { + resolve: async () => { return Order.find({}).sort({createTime: 1}) } }, @@ -116,15 +214,15 @@ export function schema(marketplace: Marketplace): GraphQLSchema { contractAddress: {type: GraphQLString}, tokenId: {type: GraphQLString}, }, - resolve: (_, args) => { + resolve: async (_, args) => { const {contractAddress, tokenId} = args const filter = {contractAddress, tokens: {tokenId}} return Order - .find({$or: [ - {left: {filter}}, - {right: {filter}}, - ]}) - .sort({createTime: 1}) + .find({$or: [ + {left: {filter}}, + {right: {filter}}, + ]}) + .sort({createTime: 1}) } }, @@ -145,6 +243,17 @@ export function schema(marketplace: Marketplace): GraphQLSchema { await marketplace.createOrder(order); return true; } + }, + + uploadFile: { + type: GraphQLBoolean, + args: {file: {type: GraphQLUpload}}, + resolve: async (_, args) => { + const { createReadStream, filename, mimetype, encoding } = await args.file; + let stream = createReadStream(); + // stream.on("readable", () => {console.log(stream.read())}); + return true; + } } } }) diff --git a/lib/event_logger.ts b/lib/event_logger.ts index b695b78..835dbb5 100644 --- a/lib/event_logger.ts +++ b/lib/event_logger.ts @@ -13,7 +13,7 @@ const interfaceId: { [key in TokenType]?: string } = { [TokenType.ERC721]: "0x80ac58cd", } -const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +const ZERO_ADDRESS = ethers.constants.AddressZero; const IPFS_GATEWAYS = [ "https://gateway.pinata.cloud/ipfs/", "https://cloudflare-ipfs.com/ipfs/", diff --git a/lib/types/mongo.ts b/lib/types/mongo.ts index 4eabc2f..23bcf35 100644 --- a/lib/types/mongo.ts +++ b/lib/types/mongo.ts @@ -1,4 +1,5 @@ import { model, Schema } from 'mongoose'; +const paginationPlugin = require('@mother/mongoose-cursor-pagination') // todo https://mongoosejs.com/docs/typescript.html @@ -27,6 +28,8 @@ const TokenTransferEventSchema = new Schema({ }, { _id: false }); const TokenSchema = new Schema({ + collectionObjectId: String, + tokenId: String, metadata_uri: String, @@ -35,15 +38,15 @@ const TokenSchema = new Schema({ last_update: Number, owners: { type: Map, of: Number }, events: [TokenTransferEventSchema], -}, { _id: false }) +}).plugin(paginationPlugin); + +export const Tokens = model("Tokens", TokenSchema); export const TokensCollection = model('TokensCollection', new Schema({ contractAddress: String, tokenType: Number, name: String, owner: String, - - tokens: [TokenSchema], })); diff --git a/package.json b/package.json index c62ed49..204a036 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "private": false, "scripts": { "api": "ts-node ./lib/main.ts", + "gen fake data": "ts-node ./scripts/gen_fake_tokens.ts", "build": "hardhat compile", "deploy": "hardhat deploy --network rinkeby", "verify": "hardhat etherscan-verify --network rinkeby", @@ -36,17 +37,21 @@ }, "dependencies": { "@gnosis.pm/safe-deployments": "^1.1.0", + "@mother/mongoose-cursor-pagination": "^0.0.5", "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^4.1.0", "@pinata/sdk": "^1.1.23", "@types/dotenv": "^8.2.0", "@types/mongoose": "^5.11.97", + "apollo-server": "^3.5.0", + "apollo-server-express": "^3.0.0-rc.1", "dotenv": "^9.0.2", "ethereum-waffle": "^3.2.0", "ethers": "^5.5.1", "express-graphql": "^0.12.0", "graphql": "^16.0.1", + "graphql-upload": "^13.0.0", "hardhat": "^2.3.3", "hardhat-deploy": "^0.8.11", "hardhat-deploy-ethers": "0.3.0-beta.10", diff --git a/test/utils/gen_fake_tokens.ts b/scripts/gen_fake_tokens.ts similarity index 58% rename from test/utils/gen_fake_tokens.ts rename to scripts/gen_fake_tokens.ts index 1f8dc98..513c56a 100644 --- a/test/utils/gen_fake_tokens.ts +++ b/scripts/gen_fake_tokens.ts @@ -1,32 +1,42 @@ -import {TokensCollection} from "../../lib/types/mongo"; +import {TokensCollection, Tokens} from "../lib/types/mongo"; import {ethers} from "ethers"; -import {zero} from "./utils"; -import {TokenType} from "../../lib/types/common"; +import {zero} from "../test/utils/utils"; +import {TokenType} from "../lib/types/common"; +import amongus from "mongoose"; +amongus.connect('mongodb://root:example@localhost:27017/admin'); -randomCollection() +generateFakeData(2); -function randomCollection() { - const tokens = []; - for (let i=0; i<10; i++) - tokens.push(randomToken()) +function generateFakeData(tokensAmount: number) { + let collectionObjId; new TokensCollection({ contractAddress: randomAddress(), tokenType: randomChoice([TokenType.ERC1155, TokenType.ERC721]), owner: randomAddress(), - tokens: [] - }).save().then((r:any) => console.log(r)); + }).save().then((r:any) => { + collectionObjId = r._id + const tokens = []; + for (let i=0; i { + new Tokens(value).save(); + }); + }); } -function randomToken() { +function randomToken(collectionObjId: any) { const tokenId = randomFrom0To(1000).toString(); - const quantity = randomFrom0To(100) - const addr = randomAddress() + const quantity = randomFrom0To(100); + const addr = randomAddress(); return { + collectionObjectId: collectionObjId, tokenId: tokenId, metadata_uri: `http://localhost/${tokenId}`, metadata: @@ -56,14 +66,17 @@ function randomToken() { } function randomAddress() { - return "0x" + ethers.utils.keccak256(randomFrom0To(100000).toString()).toString().slice(0, 40) + return randomHash().slice(0, 42) } + function randomHash() { - return "0x" + ethers.utils.keccak256(randomFrom0To(100000).toString()).toString().slice(0, 64) + return ethers.utils.hashMessage(randomFrom0To(100000).toString()) } + function randomChoice(items: any[]) { return items[randomFrom0To(items.length)]; } + function randomFrom0To(value: number) { return Math.floor(Math.random() * value) } diff --git a/test/utils/utils.ts b/test/utils/utils.ts index 8eac21a..5b64dfe 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -1,5 +1,6 @@ import chai from "chai"; import chaiAsPromised from "chai-as-promised"; +import {constants} from "ethers"; chai.should(); @@ -7,7 +8,7 @@ chai.use(chaiAsPromised); export const expect = chai.expect; -export const zero = "0x0000000000000000000000000000000000000000" +export const zero = constants.AddressZero; export const endtime = (d: number) => {