diff --git a/examples/harvester/src/App.tsx b/examples/harvester/src/App.tsx index ae5afa23..ab22c24b 100644 --- a/examples/harvester/src/App.tsx +++ b/examples/harvester/src/App.tsx @@ -134,7 +134,8 @@ export const App = () => { amount, )} MAGIC from connected wallet to smart account`, ); - await tdk?.contract.write(contractAddresses.MAGIC, { + await tdk?.transaction.create({ + address: contractAddresses.MAGIC, abi: erc20Abi, functionName: "transferFrom", args: [eoaAddress, smartAccountAddress, amount], @@ -149,7 +150,8 @@ export const App = () => { addLog( "Transferring Ancient Permit from connected wallet to smart account", ); - await tdk?.contract.write(contractAddresses.Consumables, { + await tdk?.transaction.create({ + address: contractAddresses.Consumables, abi: erc1155Abi, functionName: "safeTransferFrom", args: [eoaAddress, smartAccountAddress, permitTokenId, 1n, zeroHash], @@ -159,7 +161,8 @@ export const App = () => { // Queue Consumables-NftHandler approval addLog("Approving Harvester to transfer Consumables"); - await tdk?.contract.write(contractAddresses.Consumables, { + await tdk?.transaction.create({ + address: contractAddresses.Consumables, abi: erc1155Abi, functionName: "setApprovalForAll", args: [nftHandlerAddress, true], @@ -168,7 +171,8 @@ export const App = () => { // Queue Ancient Permit deposit addLog("Staking Ancient Permit to Harvester"); - await tdk?.contract.write(nftHandlerAddress, { + await tdk?.transaction.create({ + address: nftHandlerAddress, abi: nftHandlerAbi, functionName: "stakeNft", args: [contractAddresses.Consumables, permitTokenId, 1n], @@ -178,7 +182,8 @@ export const App = () => { // Queue MAGIC-Harvester approval addLog("Approving Harvester to transfer MAGIC"); - await tdk?.contract.write(contractAddresses.MAGIC, { + await tdk?.transaction.create({ + address: contractAddresses.MAGIC, abi: erc20Abi, functionName: "approve", args: [harvesterAddress, amount], @@ -187,7 +192,8 @@ export const App = () => { // // Queue Harvester deposit addLog(`Depositing ${formatEther(amount)} MAGIC to Harvester`); - await tdk?.contract.write(harvesterAddress, { + await tdk?.transaction.create({ + address: harvesterAddress, abi: harvesterAbi, functionName: "deposit", args: [amount, 0n], @@ -200,7 +206,8 @@ export const App = () => { const handleWithdraw = async () => { if (harvesterDeposit > 0) { addLog("Withdrawing all MAGIC from Harvester"); - await tdk?.contract.write(harvesterAddress, { + await tdk?.transaction.create({ + address: harvesterAddress, abi: harvesterAbi, functionName: "withdrawAll", args: [], @@ -210,7 +217,8 @@ export const App = () => { if (harvesterPermits > 0) { addLog("Withdrawing all Ancient Permits from Harvester"); - await tdk?.contract.write(nftHandlerAddress, { + await tdk?.transaction.create({ + address: nftHandlerAddress, abi: nftHandlerAbi, functionName: "unstakeNft", args: [contractAddresses.Consumables, permitTokenId, harvesterPermits], @@ -224,7 +232,8 @@ export const App = () => { const handleTransfer = async () => { if (smartAccountMagic > 0) { addLog("Transferring all MAGIC from smart account to connected wallet"); - await tdk?.contract.write(contractAddresses.MAGIC, { + await tdk?.transaction.create({ + address: contractAddresses.MAGIC, abi: erc20Abi, functionName: "transfer", args: [eoaAddress, smartAccountMagic], @@ -236,7 +245,8 @@ export const App = () => { addLog( "Transferring all Ancient Permits from smart account to connected wallet", ); - await tdk?.contract.write(contractAddresses.Consumables, { + await tdk?.transaction.create({ + address: contractAddresses.Consumables, abi: erc1155Abi, functionName: "safeTransferFrom", args: [ diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index e5511658..454ff926 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -9,7 +9,6 @@ import { withCors } from "./middleware/cors"; import { withErrorHandler } from "./middleware/error"; import { withProject } from "./middleware/project"; import { withSwagger } from "./middleware/swagger"; -import { contractsRoutes } from "./routes/contracts"; import { harvestersRoutes } from "./routes/harvesters"; import { projectsRoutes } from "./routes/projects"; import { transactionsRoutes } from "./routes/transactions"; @@ -46,7 +45,6 @@ const main = async () => { // Routes await Promise.all([ app.register(projectsRoutes(ctx)), - app.register(contractsRoutes(ctx)), app.register(transactionsRoutes(ctx)), app.register(harvestersRoutes), ]); diff --git a/packages/api/src/routes/contracts.ts b/packages/api/src/routes/contracts.ts deleted file mode 100644 index 115930c2..00000000 --- a/packages/api/src/routes/contracts.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { type Static, Type } from "@sinclair/typebox"; -import type { FastifyPluginAsync } from "fastify"; - -import { getUser } from "../middleware/auth"; -import "../middleware/chain"; -import "../middleware/project"; -import type { TdkApiContext } from "../types"; -import { type ErrorReply, baseReplySchema } from "../utils/schema"; - -const writeContractParamsSchema = Type.Object({ - address: Type.String(), -}); - -const writeContractBodySchema = Type.Object({ - functionName: Type.String(), - args: Type.Any(), -}); - -const writeContractReplySchema = Type.Object({ - queueId: Type.String(), -}); - -export type WriteContractParams = Static; -export type WriteContractBody = Static; -export type WriteContractReply = - | Static - | ErrorReply; - -export const contractsRoutes = - ({ engine }: TdkApiContext): FastifyPluginAsync => - async (app) => { - app.post<{ - Params: WriteContractParams; - Body: WriteContractBody; - Reply: WriteContractReply; - }>( - "/contracts/:address", - { - schema: { - response: { - ...baseReplySchema, - 200: writeContractReplySchema, - }, - }, - }, - async (req, reply) => { - const user = await getUser(req); - if (!user) { - return reply.code(401).send({ error: "Unauthorized" }); - } - - const { - chainId, - backendWallet, - params: { address }, - body, - } = req; - try { - const { result } = await engine.contract.write( - chainId.toString(), - address, - backendWallet, - body, - false, - user.address, - ); - reply.send(result); - } catch (err) { - console.error("Contract write error:", err); - if (err instanceof Error) { - reply.code(500).send({ error: err.message }); - } - } - }, - ); - }; diff --git a/packages/api/src/routes/harvesters.ts b/packages/api/src/routes/harvesters.ts index 002343b3..dc1b26b5 100644 --- a/packages/api/src/routes/harvesters.ts +++ b/packages/api/src/routes/harvesters.ts @@ -20,19 +20,17 @@ const readHarvesterParamsSchema = Type.Object({ }); const readHarvesterReplySchema = Type.Object({ - harvester: Type.Object({ - nftHandlerAddress: Type.String(), - permitsAddress: Type.String(), - permitsTokenId: Type.String(), - }), - user: Type.Object({ - magicBalance: Type.String(), - permitsBalance: Type.Number(), - harvesterMagicAllowance: Type.String(), - harvesterPermitsApproved: Type.Boolean(), - harvesterDepositCap: Type.String(), - harvesterDepositAmount: Type.String(), - }), + id: Type.String(), + nftHandlerAddress: Type.String(), + permitsAddress: Type.String(), + permitsTokenId: Type.String(), + permitsDepositCap: Type.String(), + userMagicBalance: Type.String(), + userPermitsBalance: Type.Number(), + userMagicAllowance: Type.String(), + userApprovedPermits: Type.Boolean(), + userDepositCap: Type.String(), + userDepositAmount: Type.String(), }); export type ReadHarvesterParams = Static; @@ -59,38 +57,17 @@ export const harvestersRoutes: FastifyPluginAsync = async (app) => { chainId, params: { id }, } = req; - const user = await getUser(req); - if (!user) { - return reply.code(401).send({ error: "Unauthorized" }); - } const contractAddresses = getContractAddresses(chainId); - const smartAccountAddress = user.address as AddressString; const harvesterAddress = id as AddressString; const [ - { result: magicBalance = 0n }, - { result: magicAllowance = 0n }, { result: nftHandlerAddress = zeroAddress }, { result: [permitsAddress, permitsTokenId] = [zeroAddress, 0n] as const, }, - { result: depositCap = 0n }, - { result: [harvesterDepositAmount] = [0n] as const }, ] = await readContracts(config, { contracts: [ - { - address: contractAddresses.MAGIC, - abi: erc20Abi, - functionName: "balanceOf", - args: [smartAccountAddress], - }, - { - address: contractAddresses.MAGIC, - abi: erc20Abi, - functionName: "allowance", - args: [smartAccountAddress, harvesterAddress], - }, { address: harvesterAddress, abi: harvesterAbi, @@ -101,18 +78,6 @@ export const harvestersRoutes: FastifyPluginAsync = async (app) => { abi: harvesterAbi, functionName: "depositCapPerWallet", }, - { - address: harvesterAddress, - abi: harvesterAbi, - functionName: "getUserDepositCap", - args: [smartAccountAddress], - }, - { - address: harvesterAddress, - abi: harvesterAbi, - functionName: "getUserGlobalDeposit", - args: [smartAccountAddress], - }, ], }); @@ -120,40 +85,83 @@ export const harvestersRoutes: FastifyPluginAsync = async (app) => { return reply.code(404).send({ error: "Not found" }); } - const [ - { result: permitsBalance = 0n }, - { result: harvesterPermitsApproved = false }, - ] = await readContracts(config, { - contracts: [ - { - address: permitsAddress, - abi: erc1155Abi, - functionName: "balanceOf", - args: [smartAccountAddress, permitsTokenId], - }, - { - address: permitsAddress, - abi: erc1155Abi, - functionName: "isApprovedForAll", - args: [smartAccountAddress, nftHandlerAddress], - }, - ], - }); + let magicBalance = 0n; + let magicAllowance = 0n; + let permitsBalance = 0n; + let userApprovedPermits = false; + let depositCap = 0n; + let depositAmount = 0n; + const user = await getUser(req); + if (user) { + const smartAccountAddress = user.address as AddressString; + const [ + { result: magicBalanceResult }, + { result: magicAllowanceResult }, + { result: permitsBalanceResult }, + { result: userApprovedPermitsResult }, + { result: depositCapResult }, + { result: globalDepositResult }, + ] = await readContracts(config, { + contracts: [ + { + address: contractAddresses.MAGIC, + abi: erc20Abi, + functionName: "balanceOf", + args: [smartAccountAddress], + }, + { + address: contractAddresses.MAGIC, + abi: erc20Abi, + functionName: "allowance", + args: [smartAccountAddress, harvesterAddress], + }, + { + address: permitsAddress, + abi: erc1155Abi, + functionName: "balanceOf", + args: [smartAccountAddress, permitsTokenId], + }, + { + address: permitsAddress, + abi: erc1155Abi, + functionName: "isApprovedForAll", + args: [smartAccountAddress, nftHandlerAddress], + }, + { + address: harvesterAddress, + abi: harvesterAbi, + functionName: "getUserDepositCap", + args: [smartAccountAddress], + }, + { + address: harvesterAddress, + abi: harvesterAbi, + functionName: "getUserGlobalDeposit", + args: [smartAccountAddress], + }, + ], + }); + + magicBalance = magicBalanceResult ?? 0n; + magicAllowance = magicAllowanceResult ?? 0n; + permitsBalance = permitsBalanceResult ?? 0n; + userApprovedPermits = userApprovedPermitsResult ?? false; + depositCap = depositCapResult ?? 0n; + depositAmount = globalDepositResult?.[0] ?? 0n; + } reply.send({ - harvester: { - nftHandlerAddress, - permitsAddress, - permitsTokenId: permitsTokenId.toString(), - }, - user: { - magicBalance: magicBalance.toString(), - permitsBalance: Number(permitsBalance), - harvesterMagicAllowance: magicAllowance.toString(), - harvesterPermitsApproved, - harvesterDepositCap: depositCap.toString(), - harvesterDepositAmount: harvesterDepositAmount.toString(), - }, + id, + nftHandlerAddress, + permitsAddress, + permitsTokenId: permitsTokenId.toString(), + permitsDepositCap: "2000", + userMagicBalance: magicBalance.toString(), + userPermitsBalance: Number(permitsBalance), + userMagicAllowance: magicAllowance.toString(), + userApprovedPermits, + userDepositCap: depositCap.toString(), + userDepositAmount: depositAmount.toString(), }); }, ); diff --git a/packages/api/src/routes/transactions.ts b/packages/api/src/routes/transactions.ts index 36afe3fe..92d96640 100644 --- a/packages/api/src/routes/transactions.ts +++ b/packages/api/src/routes/transactions.ts @@ -2,6 +2,8 @@ import { type Static, Type } from "@sinclair/typebox"; import type { FastifyPluginAsync } from "fastify"; import { getUser } from "../middleware/auth"; +import "../middleware/chain"; +import "../middleware/project"; import type { TdkApiContext } from "../types"; import { type ErrorReply, @@ -9,6 +11,16 @@ import { nullableStringSchema, } from "../utils/schema"; +const createTransactionBodySchema = Type.Object({ + address: Type.String(), + functionName: Type.String(), + args: Type.Any(), +}); + +const createTransactionReplySchema = Type.Object({ + queueId: Type.String(), +}); + const readTransactionParamsSchema = Type.Object({ queueId: Type.String(), }); @@ -19,6 +31,11 @@ const readTransactionReplySchema = Type.Object({ errorMessage: nullableStringSchema, }); +export type CreateTransactionBody = Static; +export type CreateTransactionReply = + | Static + | ErrorReply; + export type ReadTransactionParams = Static; export type ReadTransactionReply = | Static @@ -27,6 +44,46 @@ export type ReadTransactionReply = export const transactionsRoutes = ({ engine }: TdkApiContext): FastifyPluginAsync => async (app) => { + app.post<{ + Body: CreateTransactionBody; + Reply: CreateTransactionReply; + }>( + "/transactions", + { + schema: { + response: { + ...baseReplySchema, + 200: createTransactionReplySchema, + }, + }, + }, + async (req, reply) => { + const user = await getUser(req); + if (!user) { + return reply.code(401).send({ error: "Unauthorized" }); + } + + const { chainId, backendWallet, body: postBody } = req; + const { address, ...body } = postBody; + try { + const { result } = await engine.contract.write( + chainId.toString(), + address, + backendWallet, + body, + false, + user.address, + ); + reply.send(result); + } catch (err) { + console.error("Contract write error:", err); + if (err instanceof Error) { + reply.code(500).send({ error: err.message }); + } + } + }, + ); + app.get<{ Params: ReadTransactionParams; Reply: ReadTransactionReply; diff --git a/packages/api/src/sdk.ts b/packages/api/src/sdk.ts index d042ca70..800e98db 100644 --- a/packages/api/src/sdk.ts +++ b/packages/api/src/sdk.ts @@ -5,8 +5,8 @@ import type { ExtractAbiFunctionNames, } from "abitype"; -import type { WriteContractReply } from "./routes/contracts"; import type { ReadProjectReply } from "./routes/projects"; +import type { CreateTransactionReply } from "./routes/transactions"; import type { ErrorReply } from "./utils/schema"; // @ts-expect-error: Patch BigInt for JSON serialization @@ -113,32 +113,23 @@ export class TDKAPI { this.get(`/projects/${slug}`), }; - contract = { - write: < + transaction = { + create: < TAbi extends Abi, TFunctionName extends ExtractAbiFunctionNames< TAbi, "nonpayable" | "payable" >, - >( - address: string, - { - functionName, - args, - }: { - abi: TAbi; - functionName: - | TFunctionName - | ExtractAbiFunctionNames; - args: AbiParametersToPrimitiveTypes< - ExtractAbiFunction["inputs"], - "inputs" - >; - }, - ) => - this.post(`/contracts/${address}`, { - functionName, - args, - }), + >(params: { + address: string; + abi: TAbi; + functionName: + | TFunctionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes< + ExtractAbiFunction["inputs"], + "inputs" + >; + }) => this.post(`/transactions`, params), }; }