diff --git a/wallet-earn/app/api/deposit/execute/route.ts b/wallet-earn/app/api/deposit/execute/route.ts new file mode 100644 index 00000000..7b586745 --- /dev/null +++ b/wallet-earn/app/api/deposit/execute/route.ts @@ -0,0 +1,93 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; +import { type UnsignedTransaction } from "@compass-labs/api-sdk/models/components"; +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { base } from "viem/chains"; + +export async function POST(request: Request) { + try { + const { owner, eip712, signature } = await request.json(); + + if (!owner || !eip712 || !signature) { + return Response.json( + { error: "Missing required parameters (owner, eip712, signature)" }, + { status: 400 } + ); + } + + // Sponsor account - pays gas and submits transaction + const sponsorAccount = privateKeyToAccount( + process.env.GAS_SPONSOR_PK as `0x${string}` + ); + + const sponsorWalletClient = createWalletClient({ + account: sponsorAccount, + chain: base, + transport: http(process.env.RPC_URL), + }); + + const publicClient = createPublicClient({ + chain: base, + transport: http(process.env.RPC_URL), + }); + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Prepare gas-sponsored transaction with user's signature + const sponsorGasResponse = await compassApiSDK.gasSponsorship.gasSponsorshipPrepare({ + owner, + chain: CHAIN, + eip712: eip712 as any, + signature, + sender: sponsorAccount.address, + }); + + const sponsoredTransaction = sponsorGasResponse.transaction as UnsignedTransaction; + + if (!sponsoredTransaction) { + return Response.json( + { error: "No transaction returned from gasSponsorshipPrepare" }, + { status: 500 } + ); + } + + // Sponsor signs and submits the transaction + const depositTxHash = await sponsorWalletClient.sendTransaction({ + ...(sponsoredTransaction as any), + value: BigInt(sponsoredTransaction.value), + gas: sponsoredTransaction.gas ? BigInt(sponsoredTransaction.gas) : undefined, + maxFeePerGas: BigInt(sponsoredTransaction.maxFeePerGas), + maxPriorityFeePerGas: BigInt(sponsoredTransaction.maxPriorityFeePerGas), + }); + + const tx = await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, + }); + + if (tx.status !== "success") { + return Response.json( + { error: "Deposit transaction reverted" }, + { status: 500 } + ); + } + + return Response.json({ + success: true, + txHash: depositTxHash, + }); + } catch (error) { + console.error("Error executing deposit:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to execute deposit" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/api/deposit/prepare/route.ts b/wallet-earn/app/api/deposit/prepare/route.ts new file mode 100644 index 00000000..3effc90b --- /dev/null +++ b/wallet-earn/app/api/deposit/prepare/route.ts @@ -0,0 +1,66 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; + +export async function POST(request: Request) { + try { + const { vaultAddress, amount, token, owner } = await request.json(); + + if (!vaultAddress || !amount || !token || !owner) { + return Response.json( + { error: "Missing required parameters" }, + { status: 400 } + ); + } + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Call earnManage with gas sponsorship enabled + const deposit = await compassApiSDK.earn.earnManage({ + owner, + chain: CHAIN, + venue: { + type: "VAULT", + vaultAddress, + }, + action: "DEPOSIT", + amount, + gasSponsorship: true, + }); + + const eip712TypedData = deposit.eip712; + + if (!eip712TypedData) { + return Response.json( + { error: "No EIP-712 typed data returned from earnManage" }, + { status: 500 } + ); + } + + // Normalize types for viem compatibility + // SDK returns camelCase keys (safeTx, eip712Domain) but primaryType as "SafeTx" + const normalizedTypes = { + EIP712Domain: (eip712TypedData.types as any).eip712Domain, + SafeTx: (eip712TypedData.types as any).safeTx, + }; + + return Response.json({ + eip712: eip712TypedData, + normalizedTypes, + domain: eip712TypedData.domain, + message: eip712TypedData.message, + }); + } catch (error) { + console.error("Error preparing deposit:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to prepare deposit" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/api/earn-account/check/route.ts b/wallet-earn/app/api/earn-account/check/route.ts new file mode 100644 index 00000000..dda8c6f5 --- /dev/null +++ b/wallet-earn/app/api/earn-account/check/route.ts @@ -0,0 +1,97 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; +import { createPublicClient, http } from "viem"; +import { base } from "viem/chains"; + +const publicClient = createPublicClient({ + chain: base, + transport: http(process.env.RPC_URL), +}); + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const owner = searchParams.get("owner"); + + if (!owner) { + return Response.json( + { error: "Missing owner address" }, + { status: 400 } + ); + } + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Try to create account - if it returns 400, account already exists + try { + const response = await compassApiSDK.earn.earnCreateAccount({ + chain: CHAIN, + owner, + sender: owner, + estimateGas: false, + }); + + const earnAccountAddress = response.earnAccountAddress; + + // Check if account is already deployed by checking bytecode + const bytecode = await publicClient.getCode({ + address: earnAccountAddress as `0x${string}`, + }); + + const isDeployed = bytecode !== undefined && bytecode !== "0x"; + + return Response.json({ + earnAccountAddress, + isDeployed, + }); + } catch (sdkError: any) { + // If SDK returns 400, account already exists - this means it's deployed + // Try to extract the earn account address from the error or response + console.log("SDK error checking earn account:", sdkError); + + // Check if it's a 400 error (account already exists) + const statusCode = sdkError?.statusCode || sdkError?.status; + if (statusCode === 400) { + // Account exists - try to get the address from the error body + const errorBody = sdkError?.body || sdkError?.rawResponse; + let earnAccountAddress = null; + + // Try to parse earn_account_address from error response + if (typeof errorBody === "string") { + try { + const parsed = JSON.parse(errorBody); + earnAccountAddress = parsed.earn_account_address || parsed.earnAccountAddress; + } catch { + // Ignore parse errors + } + } else if (errorBody?.earn_account_address) { + earnAccountAddress = errorBody.earn_account_address; + } + + return Response.json({ + earnAccountAddress, + isDeployed: true, // 400 means account already exists + }); + } + + return Response.json({ + earnAccountAddress: null, + isDeployed: false, + error: "Could not determine earn account status", + }); + } + } catch (error) { + console.error("Error checking earn account:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to check earn account" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/api/earn-account/create/route.ts b/wallet-earn/app/api/earn-account/create/route.ts new file mode 100644 index 00000000..654f5278 --- /dev/null +++ b/wallet-earn/app/api/earn-account/create/route.ts @@ -0,0 +1,44 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; + +export async function POST(request: Request) { + try { + const { owner } = await request.json(); + + if (!owner) { + return Response.json( + { error: "Missing owner address" }, + { status: 400 } + ); + } + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Call Compass API to create earn account + // The owner will also be the sender (they sign and submit the tx) + const response = await compassApiSDK.earn.earnCreateAccount({ + chain: CHAIN, + owner, + sender: owner, // Owner sends the transaction themselves + estimateGas: true, + }); + + return Response.json({ + transaction: response.transaction, + earnAccountAddress: response.earnAccountAddress, + }); + } catch (error) { + console.error("Error creating earn account:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to create earn account" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/api/positions/route.ts b/wallet-earn/app/api/positions/route.ts index e74814fd..e458517f 100644 --- a/wallet-earn/app/api/positions/route.ts +++ b/wallet-earn/app/api/positions/route.ts @@ -1,10 +1,17 @@ import { CHAIN } from "@/utils/constants"; -import { getWalletAddress } from "@/utils/utils"; import { CompassApiSDK } from "@compass-labs/api-sdk"; -export async function GET() { +export async function GET(request: Request) { try { - const walletAddress = getWalletAddress(); + const { searchParams } = new URL(request.url); + const walletAddress = searchParams.get("wallet"); + + if (!walletAddress) { + return new Response( + JSON.stringify({ error: "Missing wallet address" }), + { status: 400 } + ); + } const compassApiSDK = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, diff --git a/wallet-earn/app/api/token/[token]/route.ts b/wallet-earn/app/api/token/[token]/route.ts index 3f62d91e..42f809dc 100644 --- a/wallet-earn/app/api/token/[token]/route.ts +++ b/wallet-earn/app/api/token/[token]/route.ts @@ -1,14 +1,20 @@ import { CHAIN } from "@/utils/constants"; -import { getWalletAddress } from "@/utils/utils"; import { CompassApiSDK } from "@compass-labs/api-sdk"; export async function GET( - _: Request, + request: Request, { params }: { params: Promise<{ token: string }> } ) { const { token } = await params; - - const walletAddress = getWalletAddress(); + const { searchParams } = new URL(request.url); + const walletAddress = searchParams.get("wallet"); + + if (!walletAddress) { + return new Response( + JSON.stringify({ error: "Missing wallet address" }), + { status: 400 } + ); + } const compassApiSDK = new CompassApiSDK({ apiKeyAuth: process.env.COMPASS_API_KEY, diff --git a/wallet-earn/app/api/withdraw/execute/route.ts b/wallet-earn/app/api/withdraw/execute/route.ts new file mode 100644 index 00000000..80688ddc --- /dev/null +++ b/wallet-earn/app/api/withdraw/execute/route.ts @@ -0,0 +1,93 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; +import { type UnsignedTransaction } from "@compass-labs/api-sdk/models/components"; +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { base } from "viem/chains"; + +export async function POST(request: Request) { + try { + const { owner, eip712, signature } = await request.json(); + + if (!owner || !eip712 || !signature) { + return Response.json( + { error: "Missing required parameters (owner, eip712, signature)" }, + { status: 400 } + ); + } + + // Sponsor account - pays gas and submits transaction + const sponsorAccount = privateKeyToAccount( + process.env.GAS_SPONSOR_PK as `0x${string}` + ); + + const sponsorWalletClient = createWalletClient({ + account: sponsorAccount, + chain: base, + transport: http(process.env.RPC_URL), + }); + + const publicClient = createPublicClient({ + chain: base, + transport: http(process.env.RPC_URL), + }); + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Prepare gas-sponsored transaction with user's signature + const sponsorGasResponse = await compassApiSDK.gasSponsorship.gasSponsorshipPrepare({ + owner, + chain: CHAIN, + eip712: eip712 as any, + signature, + sender: sponsorAccount.address, + }); + + const sponsoredTransaction = sponsorGasResponse.transaction as UnsignedTransaction; + + if (!sponsoredTransaction) { + return Response.json( + { error: "No transaction returned from gasSponsorshipPrepare" }, + { status: 500 } + ); + } + + // Sponsor signs and submits the transaction + const withdrawTxHash = await sponsorWalletClient.sendTransaction({ + ...(sponsoredTransaction as any), + value: BigInt(sponsoredTransaction.value), + gas: sponsoredTransaction.gas ? BigInt(sponsoredTransaction.gas) : undefined, + maxFeePerGas: BigInt(sponsoredTransaction.maxFeePerGas), + maxPriorityFeePerGas: BigInt(sponsoredTransaction.maxPriorityFeePerGas), + }); + + const tx = await publicClient.waitForTransactionReceipt({ + hash: withdrawTxHash, + }); + + if (tx.status !== "success") { + return Response.json( + { error: "Withdraw transaction reverted" }, + { status: 500 } + ); + } + + return Response.json({ + success: true, + txHash: withdrawTxHash, + }); + } catch (error) { + console.error("Error executing withdraw:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to execute withdraw" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/api/withdraw/prepare/route.ts b/wallet-earn/app/api/withdraw/prepare/route.ts new file mode 100644 index 00000000..d4e9fab7 --- /dev/null +++ b/wallet-earn/app/api/withdraw/prepare/route.ts @@ -0,0 +1,65 @@ +import { CHAIN } from "@/utils/constants"; +import { CompassApiSDK } from "@compass-labs/api-sdk"; + +export async function POST(request: Request) { + try { + const { vaultAddress, amount, token, owner, isAll } = await request.json(); + + if (!vaultAddress || !amount || !token || !owner || typeof isAll !== "boolean") { + return Response.json( + { error: "Missing required parameters" }, + { status: 400 } + ); + } + + const compassApiSDK = new CompassApiSDK({ + apiKeyAuth: process.env.COMPASS_API_KEY, + }); + + // Call earnManage with gas sponsorship enabled + const withdraw = await compassApiSDK.earn.earnManage({ + owner, + chain: CHAIN, + venue: { + type: "VAULT", + vaultAddress, + }, + action: "WITHDRAW", + amount: isAll ? "ALL" : amount, + gasSponsorship: true, + }); + + const eip712TypedData = withdraw.eip712; + + if (!eip712TypedData) { + return Response.json( + { error: "No EIP-712 typed data returned from earnManage" }, + { status: 500 } + ); + } + + // Normalize types for viem compatibility + const normalizedTypes = { + EIP712Domain: (eip712TypedData.types as any).eip712Domain, + SafeTx: (eip712TypedData.types as any).safeTx, + }; + + return Response.json({ + eip712: eip712TypedData, + normalizedTypes, + domain: eip712TypedData.domain, + message: eip712TypedData.message, + }); + } catch (error) { + console.error("Error preparing withdraw:", error); + + if (error instanceof Error) { + return Response.json({ error: error.message }, { status: 400 }); + } + + return Response.json( + { error: "Failed to prepare withdraw" }, + { status: 500 } + ); + } +} diff --git a/wallet-earn/app/layout.tsx b/wallet-earn/app/layout.tsx index 471bad67..e6eaee89 100644 --- a/wallet-earn/app/layout.tsx +++ b/wallet-earn/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono, Inter } from "next/font/google"; -import { Analytics } from "@vercel/analytics/next" +import { Analytics } from "@vercel/analytics/next"; +import { PrivyProvider } from "@/lib/providers/privy-provider"; import "./globals.css"; const geistSans = Geist({ @@ -34,7 +35,9 @@ export default function RootLayout({
- {children} +