diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e4b3e08b..38382c8e 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -17,7 +17,6 @@ import { withErrorHandler } from "./middleware/error"; import { withProject } from "./middleware/project"; import { withSwagger } from "./middleware/swagger"; import { authRoutes } from "./routes/auth"; -import { contractsRoutes } from "./routes/contracts"; import { harvestersRoutes } from "./routes/harvesters"; import { projectsRoutes } from "./routes/projects"; import { transactionsRoutes } from "./routes/transactions"; @@ -87,7 +86,6 @@ const main = async () => { app.register(authRoutes(ctx)), app.register(usersRoutes(ctx)), app.register(projectsRoutes(ctx)), - app.register(contractsRoutes(ctx)), app.register(transactionsRoutes(ctx)), app.register(harvestersRoutes(ctx)), ]); diff --git a/apps/api/src/middleware/swagger.ts b/apps/api/src/middleware/swagger.ts index 24bbe279..f535aa23 100644 --- a/apps/api/src/middleware/swagger.ts +++ b/apps/api/src/middleware/swagger.ts @@ -14,6 +14,16 @@ export const withSwagger = async (app: FastifyInstance) => { "Backend APIs for the Treasure Development Kit powering the Treasure Web3 gaming ecosystem", version: "1.0.0", }, + components: { + securitySchemes: { + authToken: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "Authentication token obtained by calling POST /login", + }, + }, + }, }, swagger: { consumes: ["application/json"], @@ -28,8 +38,6 @@ export const withSwagger = async (app: FastifyInstance) => { nextSchema.tags = ["harvesters"]; } else if (url.startsWith("/projects")) { nextSchema.tags = ["projects"]; - } else if (url.startsWith("/contracts")) { - nextSchema.tags = ["contracts"]; } else if (url.startsWith("/transactions")) { nextSchema.tags = ["transactions"]; } else if (url.startsWith("/users")) { diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index b31ecc7d..f13e6fd4 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -3,6 +3,7 @@ import { DEFAULT_TDK_CHAIN_ID, decodeAuthToken } from "@treasure-dev/tdk-core"; import type { FastifyPluginAsync } from "fastify"; import "../middleware/project"; +import "../middleware/swagger"; import type { LoginBody, LoginReply, @@ -38,6 +39,8 @@ export const authRoutes = "/login/payload", { schema: { + summary: "Generate login payload", + description: "Generate a login payload for a given wallet address", querystring: readLoginPayloadQuerystringSchema, response: { 200: readLoginPayloadReplySchema, @@ -56,6 +59,8 @@ export const authRoutes = "/login", { schema: { + summary: "Log in", + description: "Log in with a signed payload", body: loginBodySchema, response: { 200: loginReplySchema, @@ -151,6 +156,9 @@ export const authRoutes = "/auth/authenticate", { schema: { + summary: "Log in via third party", + description: + "Start login session with custom authentication method", body: authenticateBodySchema, response: { 200: authenticateReplySchema, @@ -188,6 +196,9 @@ export const authRoutes = "/auth/verify", { schema: { + summary: "Verify third-party login", + description: + "Verify login session started via custom authentication method", body: authVerifyBodySchema, response: { 200: authVerifyReplySchema, diff --git a/apps/api/src/routes/contracts.ts b/apps/api/src/routes/contracts.ts deleted file mode 100644 index 7f49bf96..00000000 --- a/apps/api/src/routes/contracts.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { FastifyPluginAsync } from "fastify"; - -import "../middleware/chain"; -import "../middleware/project"; -import type { ReadContractBody, ReadContractReply } from "../schema"; -import { - type ErrorReply, - readContractBodySchema, - readContractReplySchema, -} from "../schema"; -import type { TdkApiContext } from "../types"; -import { verifyAuth } from "../utils/auth"; - -export const contractsRoutes = - ({ auth, engine }: TdkApiContext): FastifyPluginAsync => - async (app) => { - app.post<{ - Body: ReadContractBody; - Reply: ReadContractReply | ErrorReply; - }>( - "/contracts/read", - { - schema: { - body: readContractBodySchema, - response: { - 200: readContractReplySchema, - }, - }, - }, - async (req, reply) => { - const authResult = await verifyAuth(auth, req); - if (!authResult.valid) { - console.error( - "Error authenticating user for contract read:", - authResult.error, - ); - return reply - .code(401) - .send({ error: `Unauthorized: ${authResult.error}` }); - } - - const { - chainId, - body: { address, functionName, args }, - } = req; - try { - const { result } = await engine.contract.read( - functionName, - chainId.toString(), - address, - args, - ); - reply.send({ result }); - } catch (err) { - console.error("Error reading contract:", err); - if (err instanceof Error) { - reply.code(500).send({ error: err.message }); - } - } - }, - ); - }; diff --git a/apps/api/src/routes/harvesters.ts b/apps/api/src/routes/harvesters.ts index 7ebb1860..dd1e0ea7 100644 --- a/apps/api/src/routes/harvesters.ts +++ b/apps/api/src/routes/harvesters.ts @@ -8,6 +8,7 @@ import type { FastifyPluginAsync } from "fastify"; import { zeroAddress } from "viem"; import "../middleware/chain"; +import "../middleware/swagger"; import type { ReadHarvesterCorruptionRemovalParams, ReadHarvesterCorruptionRemovalReply, @@ -32,6 +33,9 @@ export const harvestersRoutes = "/harvesters/:id", { schema: { + summary: "Get Harvester details", + description: + "Get Harvester details including user info if valid authorization token is provided", response: { 200: readHarvesterReplySchema, }, @@ -85,6 +89,9 @@ export const harvestersRoutes = "/harvesters/:id/corruption-removal", { schema: { + summary: "Get Harvester Corruption Removal", + description: + "Get Corruption Removal recipes for Harvester including user info if valid authorization token is provided", response: { 200: readHarvesterCorruptionRemovalReplySchema, }, diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts index 0521467a..5d7ce624 100644 --- a/apps/api/src/routes/projects.ts +++ b/apps/api/src/routes/projects.ts @@ -1,6 +1,7 @@ import type { FastifyPluginAsync } from "fastify"; import "../middleware/chain"; +import "../middleware/swagger"; import { type ReadProjectParams, type ReadProjectReply, @@ -18,6 +19,8 @@ export const projectsRoutes = "/projects/:slug", { schema: { + summary: "Get project details", + description: "Get project details to power login experience", response: { 200: readProjectReplySchema, }, diff --git a/apps/api/src/routes/transactions.ts b/apps/api/src/routes/transactions.ts index d072d87b..b9874281 100644 --- a/apps/api/src/routes/transactions.ts +++ b/apps/api/src/routes/transactions.ts @@ -2,6 +2,7 @@ import type { FastifyPluginAsync } from "fastify"; import "../middleware/chain"; import "../middleware/project"; +import "../middleware/swagger"; import { type CreateTransactionBody, type CreateTransactionReply, @@ -25,6 +26,9 @@ export const transactionsRoutes = "/transactions", { schema: { + summary: "Write contract", + description: "Call a contract write function", + security: [{ authToken: [] }], body: createTransactionBodySchema, response: { 200: createTransactionReplySchema, @@ -71,6 +75,8 @@ export const transactionsRoutes = "/transactions/:queueId", { schema: { + summary: "Get transaction", + description: "Get transaction status by queue ID", response: { 200: readTransactionReplySchema, }, diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts index 1182be24..43681a7f 100644 --- a/apps/api/src/routes/users.ts +++ b/apps/api/src/routes/users.ts @@ -2,6 +2,7 @@ import { getAllActiveSigners } from "@treasure-dev/tdk-core"; import type { FastifyPluginAsync } from "fastify"; import "../middleware/chain"; +import "../middleware/swagger"; import { type ErrorReply, type ReadCurrentUserReply, @@ -19,6 +20,9 @@ export const usersRoutes = "/users/me", { schema: { + summary: "Get current user", + description: "Get current user profile details and on-chain sessions", + security: [{ authToken: [] }], response: { 200: readCurrentUserReplySchema, }, diff --git a/apps/api/src/schema.ts b/apps/api/src/schema.ts index e5584b20..91fb4ac1 100644 --- a/apps/api/src/schema.ts +++ b/apps/api/src/schema.ts @@ -38,6 +38,11 @@ export const baseReplySchema: object = { export type ErrorReply = Static; +const EXAMPLE_CONTRACT_ADDRESS = "0x539bde0d7dbd336b79148aa742883198bbf60342"; +const EXAMPLE_WALLET_ADDRESS = "0x0eB5B03c0303f2F47cD81d7BE4275AF8Ed347576"; +const EXAMPLE_QUEUE_ID = "5b6c941c-35ba-4c54-92db-41b39cd06b2d"; +const EXAMPLE_EMAIL_ADDRESS = "example@treasure.lol"; + // Auth const loginPayloadSchema = Type.Object({ domain: Type.String(), @@ -54,7 +59,10 @@ const loginPayloadSchema = Type.Object({ }); export const readLoginPayloadQuerystringSchema = Type.Object({ - address: Type.String(), + address: Type.String({ + description: "Smart wallet address used to generate payload", + examples: [EXAMPLE_WALLET_ADDRESS], + }), }); export const readLoginPayloadReplySchema = loginPayloadSchema; @@ -65,17 +73,26 @@ export const loginBodySchema = Type.Object({ }); export const loginReplySchema = Type.Object({ - token: Type.String(), + token: Type.String({ + description: "Authorization token for the logged in user", + }), }); export const authenticateBodySchema = Type.Object({ - email: Type.String(), - password: Type.String(), + email: Type.String({ + description: "Email address for the custom auth method", + examples: [EXAMPLE_EMAIL_ADDRESS], + }), + password: Type.String({ + description: "Password for the custom auth method", + }), }); export const authenticateReplySchema = Type.Object({ projectId: Type.String(), - token: Type.String(), + token: Type.String({ + description: "Authorization token for the custom auth method", + }), }); export const authVerifyBodySchema = Type.Object({ @@ -101,14 +118,28 @@ export type AuthVerifyReply = Static; // Harvesters const tokenSchema = Type.Object({ - address: Type.String(), - tokenId: Type.Number(), - name: Type.String(), - image: Type.String(), - imageAlt: Type.Optional(Type.String()), + address: Type.String({ + description: "Token contract address", + }), + tokenId: Type.Number({ + description: "Token ID", + }), + name: Type.String({ + description: "Token name", + }), + image: Type.String({ + description: "Token image URL", + }), + imageAlt: Type.Optional( + Type.String({ + description: "Alternative token image URL", + }), + ), attributes: Type.Array( Type.Object({ - type: Type.String(), + type: Type.String({ + description: "Attribute name", + }), value: Type.Union([Type.String(), Type.Number()]), }), ), @@ -117,14 +148,20 @@ const tokenSchema = Type.Object({ const inventoryTokenSchema = Type.Intersect([ tokenSchema, Type.Object({ - user: Type.String(), - balance: Type.Number(), + user: Type.String({ + description: "Token owner address", + }), + balance: Type.Number({ + description: "Token balance for owner", + }), }), ]); const corruptionRemovalRecipeSchema = Type.Object({ id: Type.String(), - corruptionRemoved: Type.String(), + corruptionRemoved: Type.String({ + description: "Amount of Corruption removed by recipe", + }), items: Type.Array( Type.Object({ address: Type.String(), @@ -147,32 +184,84 @@ const corruptionRemovalSchema = Type.Object({ const harvesterInfoSchema = Type.Object({ id: Type.String(), // NFT Handler - nftHandlerAddress: Type.String(), + nftHandlerAddress: Type.String({ + description: "Contract address of the Harvester's NFT handler", + }), // Staking Rules - permitsStakingRulesAddress: Type.String(), - boostersStakingRulesAddress: Type.String(), - legionsStakingRulesAddress: Type.Optional(Type.String()), - treasuresStakingRulesAddress: Type.Optional(Type.String()), - charactersStakingRulesAddress: Type.Optional(Type.String()), + permitsStakingRulesAddress: Type.String({ + description: + "Contract address of the Harvester's Ancient Permits staking rules", + }), + boostersStakingRulesAddress: Type.String({ + description: "Contract address of the Harvester's Boosters staking rules", + }), + legionsStakingRulesAddress: Type.Optional( + Type.String({ + description: + "Contract address of the Harvester's Legions staking rules, if applicable", + }), + ), + treasuresStakingRulesAddress: Type.Optional( + Type.String({ + description: + "Contract address of the Harvester's Treasures staking rules, if applicable", + }), + ), + charactersStakingRulesAddress: Type.Optional( + Type.String({ + description: + "Contract address of the Harvester's characters staking rules, if applicable", + }), + ), // NFTs settings - charactersAddress: Type.Optional(Type.String()), + charactersAddress: Type.Optional( + Type.String({ + description: + "Contract address of the Harvester's characters NFT, if applicable", + }), + ), // Permits settings - permitsAddress: Type.String(), - permitsTokenId: Type.Number(), - permitsMaxStakeable: Type.Number(), - permitsMagicMaxStakeable: Type.String(), + permitsAddress: Type.String({ + description: "Contract address of the Harvester's Ancient Permits", + }), + permitsTokenId: Type.Number({ + description: "Token ID of the Harvester's Ancient Permits", + }), + permitsMaxStakeable: Type.Number({ + description: "Maximum amount of Ancient Permits that can be staked", + }), + permitsMagicMaxStakeable: Type.String({ + description: + "Maximum amount of MAGIC that can be staked per Ancient Permit", + }), // Boosters settings - boostersMaxStakeable: Type.Number(), + boostersMaxStakeable: Type.Number({ + description: "Maximum amount of Boosters that can be staked at once", + }), // MAGIC settings - magicMaxStakeable: Type.String(), + magicMaxStakeable: Type.String({ + description: "Maximum amount of MAGIC that can be staked", + }), // Corruption settings - corruptionMaxGenerated: Type.String(), + corruptionMaxGenerated: Type.String({ + description: "Maximum Corruption level", + }), // Overall state - totalEmissionsActivated: Type.Number(), - totalMagicStaked: Type.String(), - totalBoost: Type.Number(), - totalBoostersBoost: Type.Number(), - totalCorruption: Type.String(), + totalEmissionsActivated: Type.Number({ + description: "Percentage of emissions currently activated", + }), + totalMagicStaked: Type.String({ + description: "Total amount of MAGIC staked", + }), + totalBoost: Type.Number({ + description: "Total boost / mining power", + }), + totalBoostersBoost: Type.Number({ + description: "Total boost from Boosters", + }), + totalCorruption: Type.String({ + description: "Current Corruption level", + }), // Boosters state boosters: Type.Array( Type.Object({ @@ -184,32 +273,85 @@ const harvesterInfoSchema = Type.Object({ }); const harvesterUserInfoSchema = Type.Object({ - userMagicBalance: Type.String(), - userMagicAllowance: Type.String(), - userPermitsBalance: Type.Number(), - userPermitsApproved: Type.Boolean(), - userBoostersBalances: Type.Record(Type.Number(), Type.Number()), - userBoostersApproved: Type.Boolean(), + userMagicBalance: Type.String({ + description: "User's MAGIC balance", + }), + userMagicAllowance: Type.String({ + description: "Allowance of user's MAGIC approval for the Harvester", + }), + userPermitsBalance: Type.Number({ + description: "Current user's Ancient Permits balance", + }), + userPermitsApproved: Type.Boolean({ + description: + "True if user has approved the Harvester to transfer Ancient Permits", + }), + userBoostersBalances: Type.Record( + Type.Number({ + description: "Booster token ID", + }), + Type.Number({ + description: "User's booster balance", + }), + ), + userBoostersApproved: Type.Boolean({ + description: "True if user has approved the Harvester to transfer Boosters", + }), userInventoryBoosters: Type.Array(inventoryTokenSchema), - userTotalBoost: Type.Number(), - userPermitsMaxStakeable: Type.Number(), - userPermitsStaked: Type.Number(), + userTotalBoost: Type.Number({ + description: "User's total boost / mining power", + }), + userPermitsMaxStakeable: Type.Number({ + description: + "Maximum amount of Ancient Permits that can be staked by a single user", + }), + userPermitsStaked: Type.Number({ + description: "Amount of Ancient Permits staked by the user", + }), userInventoryCharacters: Type.Array(inventoryTokenSchema), userStakedCharacters: Type.Array(tokenSchema), - userCharactersApproved: Type.Boolean(), - userCharactersMaxStakeable: Type.Number(), - userCharactersStaked: Type.Number(), - userCharactersMaxBoost: Type.Number(), - userCharactersBoost: Type.Number(), + userCharactersApproved: Type.Boolean({ + description: + "True if user has approved the Harvester to transfer the characters NFT", + }), + userCharactersMaxStakeable: Type.Number({ + description: + "Maximum amount of characters that can be staked by a single user", + }), + userCharactersStaked: Type.Number({ + description: "Amount of characters staked by the user", + }), + userCharactersMaxBoost: Type.Number({ + description: + "Maximum boost that can be achieved by staking characters for a single user", + }), + userCharactersBoost: Type.Number({ + description: "User's total boost from characters", + }), userInventoryLegions: Type.Array(inventoryTokenSchema), userStakedLegions: Type.Array(tokenSchema), - userLegionsApproved: Type.Boolean(), - userLegionsMaxWeightStakeable: Type.Number(), - userLegionsWeightStaked: Type.Number(), - userLegionsBoost: Type.Number(), - userMagicMaxStakeable: Type.String(), - userMagicStaked: Type.String(), - userMagicRewardsClaimable: Type.String(), + userLegionsApproved: Type.Boolean({ + description: "True if user has approved the Harvester to transfer Legions", + }), + userLegionsMaxWeightStakeable: Type.Number({ + description: + "Maximum weight of Legions that can be staked by a single user", + }), + userLegionsWeightStaked: Type.Number({ + description: "Weight of Legions staked by the user", + }), + userLegionsBoost: Type.Number({ + description: "User's total boost from Legions", + }), + userMagicMaxStakeable: Type.String({ + description: "Maximum amount of MAGIC that can be staked by the user", + }), + userMagicStaked: Type.String({ + description: "Amount of MAGIC staked by the user", + }), + userMagicRewardsClaimable: Type.String({ + description: "Amount of MAGIC rewards claimable by the user", + }), }); export const readHarvesterParamsSchema = Type.Object({ @@ -296,14 +438,17 @@ export type ReadContractReply = Static; export const createTransactionBodySchema = Type.Object({ address: Type.String({ description: "The address of the contract to call", + examples: [EXAMPLE_CONTRACT_ADDRESS], }), functionName: Type.String({ description: "The function to call on the contract", + examples: ["transfer"], }), args: Type.Array( Type.Union([ Type.String({ description: "The arguments to call on the function", + examples: [[EXAMPLE_WALLET_ADDRESS, "1000000000000000000"]], }), Type.Tuple([Type.String(), Type.String()]), Type.Object({}), @@ -342,7 +487,10 @@ export const createTransactionBodySchema = Type.Object({ }); export const createTransactionReplySchema = Type.Object({ - queueId: Type.String(), + queueId: Type.String({ + description: "The transaction queue ID", + examples: [EXAMPLE_QUEUE_ID], + }), }); export const readTransactionParamsSchema = Type.Object({