diff --git a/app/actions/index.ts b/app/actions/index.ts index dacdaa7..9816fa8 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -20,13 +20,10 @@ import { encodedRedirect } from "@/lib/utils/utils"; import { createClient } from "@/lib/utils/supabase/server"; +import { createWalletSet, createWallet } from "@/lib/wallet-provisioning"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; -const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL - ? process.env.NEXT_PUBLIC_VERCEL_URL - : "http://localhost:3000"; - export const signUpAction = async (formData: FormData) => { const email = formData.get("email")?.toString(); const password = formData.get("password")?.toString(); @@ -61,29 +58,8 @@ export const signUpAction = async (formData: FormData) => { } try { - const createdWalletSetResponse = await fetch(`${baseUrl}/api/wallet-set`, { - method: "PUT", - body: JSON.stringify({ - entityName: email, - }), - headers: { - "Content-Type": "application/json", - }, - }); - - const createdWalletSet = await createdWalletSetResponse.json(); - - const createdWalletResponse = await fetch(`${baseUrl}/api/wallet`, { - method: "POST", - body: JSON.stringify({ - walletSetId: createdWalletSet.id, - }), - headers: { - "Content-Type": "application/json", - }, - }); - - const createdWallet = await createdWalletResponse.json(); + const createdWalletSet = await createWalletSet(email); + const createdWallet = await createWallet(createdWalletSet.id); const { data: profileData, error: profileError } = await supabase .from("profiles") diff --git a/app/api/contracts/escrow/route.ts b/app/api/contracts/escrow/route.ts index eb66a49..57e5f45 100644 --- a/app/api/contracts/escrow/route.ts +++ b/app/api/contracts/escrow/route.ts @@ -72,6 +72,11 @@ async function waitForTransactionStatus(id: string) { export async function POST(req: NextRequest) { try { const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const body: CreateEscrowRequest = await req.json(); // Validate request @@ -190,6 +195,12 @@ export async function POST(req: NextRequest) { export async function GET(req: NextRequest) { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { searchParams } = new URL(req.url); const id = searchParams.get("id"); diff --git a/app/api/wallet-set/route.ts b/app/api/wallet-set/route.ts index 2cf9b5d..5a3203a 100644 --- a/app/api/wallet-set/route.ts +++ b/app/api/wallet-set/route.ts @@ -17,10 +17,17 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; +import { createWalletSet } from "@/lib/wallet-provisioning"; export async function PUT(req: NextRequest) { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { entityName } = await req.json(); if (!entityName.trim()) { @@ -30,18 +37,9 @@ export async function PUT(req: NextRequest) { ); } - const response = await circleDeveloperSdk.createWalletSet({ - name: entityName, - }); - - if (!response.data) { - return NextResponse.json( - "The response did not include a valid wallet set", - { status: 500 } - ); - } + const walletSet = await createWalletSet(entityName); - return NextResponse.json({ ...response.data.walletSet }, { status: 201 }); + return NextResponse.json({ ...walletSet }, { status: 201 }); } catch (error: any) { console.error(`Wallet set creation failed: ${error.message}`); return NextResponse.json( diff --git a/app/api/wallet/balance/request/route.ts b/app/api/wallet/balance/request/route.ts index b6d9ccd..58269cf 100644 --- a/app/api/wallet/balance/request/route.ts +++ b/app/api/wallet/balance/request/route.ts @@ -17,9 +17,16 @@ */ import { type NextRequest, NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; export async function POST(req: NextRequest) { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const body = await req.json(); if (!body.walletAddress) { diff --git a/app/api/wallet/balance/route.ts b/app/api/wallet/balance/route.ts index 453c415..7425f2a 100644 --- a/app/api/wallet/balance/route.ts +++ b/app/api/wallet/balance/route.ts @@ -17,6 +17,7 @@ */ import { type NextRequest, NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; import { z } from "zod"; @@ -35,6 +36,12 @@ export async function POST( req: NextRequest, ): Promise> { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const body = await req.json(); const parseResult = WalletIdSchema.safeParse(body); @@ -47,6 +54,25 @@ export async function POST( const { walletId } = parseResult.data; + const { data: profile } = await supabase + .from("profiles") + .select("id") + .eq("auth_user_id", user.id) + .single(); + if (!profile) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { data: walletRecord } = await supabase + .from("wallets") + .select("id") + .eq("circle_wallet_id", walletId) + .eq("profile_id", profile.id) + .single(); + if (!walletRecord) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + const response = await circleDeveloperSdk.getWalletTokenBalance({ id: walletId, includeAll: true, diff --git a/app/api/wallet/route.ts b/app/api/wallet/route.ts index 3c8c92d..541610d 100644 --- a/app/api/wallet/route.ts +++ b/app/api/wallet/route.ts @@ -16,12 +16,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Blockchain } from "@circle-fin/smart-contract-platform"; import { NextRequest, NextResponse } from "next/server"; -import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; +import { createWallet } from "@/lib/wallet-provisioning"; export async function POST(req: NextRequest) { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const { walletSetId } = await req.json(); if (!walletSetId) { @@ -31,27 +37,9 @@ export async function POST(req: NextRequest) { ); } - if (!process.env.CIRCLE_BLOCKCHAIN) { - throw new Error("CIRCLE_BLOCKCHAIN environment variable is not set"); - } - - const response = await circleDeveloperSdk.createWallets({ - accountType: "SCA", - blockchains: [process.env.CIRCLE_BLOCKCHAIN as Blockchain], - count: 1, - walletSetId, - }); - - if (!response.data?.wallets?.length) { - return NextResponse.json( - { error: "No wallets were created" }, - { status: 500 } - ); - } - - const [createdWallet] = response.data.wallets; + const wallet = await createWallet(walletSetId); - return NextResponse.json(createdWallet, { status: 201 }); + return NextResponse.json(wallet, { status: 201 }); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return NextResponse.json( diff --git a/app/api/wallet/transactions/[id]/route.ts b/app/api/wallet/transactions/[id]/route.ts index b1ac781..50b2057 100644 --- a/app/api/wallet/transactions/[id]/route.ts +++ b/app/api/wallet/transactions/[id]/route.ts @@ -17,6 +17,7 @@ */ import { type NextRequest, NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; import { z } from "zod"; @@ -48,6 +49,31 @@ export async function GET( { params }: { params: { id: string } }, ): Promise> { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { data: profile } = await supabase + .from("profiles") + .select("id") + .eq("auth_user_id", user.id) + .single(); + if (!profile) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { data: txRecord } = await supabase + .from("transactions") + .select("id") + .eq("circle_transaction_id", params.id) + .eq("profile_id", profile.id) + .single(); + if (!txRecord) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + // Validate the transaction ID is a Circle's transaction IDs const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; diff --git a/app/api/wallet/transactions/route.ts b/app/api/wallet/transactions/route.ts index 80541f2..62dffe9 100644 --- a/app/api/wallet/transactions/route.ts +++ b/app/api/wallet/transactions/route.ts @@ -17,6 +17,7 @@ */ import { type NextRequest, NextResponse } from "next/server"; +import { createSupabaseServerClient } from "@/lib/supabase/server-client"; import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; import { z } from "zod"; @@ -52,6 +53,12 @@ export async function POST( req: NextRequest, ): Promise> { try { + const supabase = createSupabaseServerClient(); + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + const body = await req.json(); const parseResult = WalletIdSchema.safeParse(body); @@ -64,6 +71,25 @@ export async function POST( const { walletId } = parseResult.data; + const { data: profile } = await supabase + .from("profiles") + .select("id") + .eq("auth_user_id", user.id) + .single(); + if (!profile) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { data: walletRecord } = await supabase + .from("wallets") + .select("id") + .eq("circle_wallet_id", walletId) + .eq("profile_id", profile.id) + .single(); + if (!walletRecord) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + const response = await circleDeveloperSdk.listTransactions({ walletIds: [walletId], includeAll: true, diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index 8e62d19..1d2cc6d 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -17,6 +17,7 @@ */ import { createSupabaseServerClient } from "@/lib/supabase/server-client"; +import { createWalletSet, createWallet } from "@/lib/wallet-provisioning"; import { NextResponse } from "next/server"; const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL @@ -61,29 +62,8 @@ export async function GET(request: Request) { return NextResponse.redirect(`${baseUrl}/${nextUrl}`); } - const createdWalletSetResponse = await fetch(`${baseUrl}/api/wallet-set`, { - method: "PUT", - body: JSON.stringify({ - entityName: data.user.email, - }), - headers: { - "Content-Type": "application/json", - }, - }); - - const createdWalletSet = await createdWalletSetResponse.json(); - - const createdWalletResponse = await fetch(`${baseUrl}/api/wallet`, { - method: "POST", - body: JSON.stringify({ - walletSetId: createdWalletSet.id, - }), - headers: { - "Content-Type": "application/json", - }, - }); - - const createdWallet = await createdWalletResponse.json(); + const createdWalletSet = await createWalletSet(data.user.email!); + const createdWallet = await createWallet(createdWalletSet.id); await supabase .schema("public") diff --git a/lib/wallet-provisioning.ts b/lib/wallet-provisioning.ts new file mode 100644 index 0000000..22dcd08 --- /dev/null +++ b/lib/wallet-provisioning.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Blockchain, WalletSet, Wallet } from "@circle-fin/developer-controlled-wallets"; +import { circleDeveloperSdk } from "@/lib/utils/developer-controlled-wallets-client"; + +export async function createWalletSet(entityName: string): Promise { + const response = await circleDeveloperSdk.createWalletSet({ name: entityName }); + + if (!response.data?.walletSet) { + throw new Error("The response did not include a valid wallet set"); + } + + return response.data.walletSet; +} + +export async function createWallet(walletSetId: string): Promise { + if (!process.env.CIRCLE_BLOCKCHAIN) { + throw new Error("CIRCLE_BLOCKCHAIN environment variable is not set"); + } + + const response = await circleDeveloperSdk.createWallets({ + accountType: "SCA", + blockchains: [process.env.CIRCLE_BLOCKCHAIN as Blockchain], + count: 1, + walletSetId, + }); + + if (!response.data?.wallets?.length) { + throw new Error("No wallets were created"); + } + + return response.data.wallets[0]; +}