diff --git a/.snyk b/.snyk new file mode 100644 index 0000000..515f5f4 --- /dev/null +++ b/.snyk @@ -0,0 +1,7 @@ +version: v1.25.0 +ignore: + SNYK-JS-BIGINTBUFFER-3364597: + - "*": + reason: No patched version is available for this transitive dependency pulled through @bonfida/spl-name-service. + expires: 2026-08-22T00:00:00.000Z +patch: {} diff --git a/app/api/explorer/tx/route.ts b/app/api/explorer/tx/route.ts index 7c00b90..14effe7 100644 --- a/app/api/explorer/tx/route.ts +++ b/app/api/explorer/tx/route.ts @@ -3,6 +3,9 @@ import { getPaymentsExplorerTransactionUrl } from "@/lib/payments"; export async function GET(request: NextRequest) { const signature = request.nextUrl.searchParams.get("signature")?.trim(); + const customRpcEndpoint = request.nextUrl.searchParams + .get("customUrl") + ?.trim(); if (!signature) { return NextResponse.json( @@ -11,5 +14,7 @@ export async function GET(request: NextRequest) { ); } - return NextResponse.redirect(getPaymentsExplorerTransactionUrl(signature)); + return NextResponse.redirect( + getPaymentsExplorerTransactionUrl(signature, customRpcEndpoint) + ); } diff --git a/app/api/payments/shield/route.ts b/app/api/payments/shield/route.ts new file mode 100644 index 0000000..6b0ad1c --- /dev/null +++ b/app/api/payments/shield/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PublicKey } from "@solana/web3.js"; +import { + PAYMENTS_CLUSTER, + PAYMENTS_ENDPOINTS, + getPaymentsApiUrl, + getPaymentsTimeoutSignal, +} from "@/lib/payments"; +import { getPaymentsErrorMessage } from "@/lib/payments-errors"; + +interface ShieldBuildRequest { + mode?: "shield" | "unshield"; + owner?: string; + mint?: string; + amount?: string; +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json()) as ShieldBuildRequest; + const { mode, owner, mint, amount } = body; + + if ( + (mode !== "shield" && mode !== "unshield") || + typeof owner !== "string" || + typeof mint !== "string" || + typeof amount !== "string" + ) { + return NextResponse.json( + { error: "Missing or invalid shield parameters" }, + { status: 400 } + ); + } + + try { + new PublicKey(owner); + new PublicKey(mint); + } catch { + return NextResponse.json( + { error: "Invalid owner or mint public key" }, + { status: 400 } + ); + } + + if (!/^[1-9]\d*$/.test(amount)) { + return NextResponse.json( + { error: "amount must be a positive integer string" }, + { status: 400 } + ); + } + + const amountBigInt = BigInt(amount); + if (amountBigInt > BigInt(Number.MAX_SAFE_INTEGER)) { + return NextResponse.json( + { error: "amount exceeds the maximum supported integer size" }, + { status: 400 } + ); + } + + const endpoint = + mode === "shield" + ? PAYMENTS_ENDPOINTS.deposit + : PAYMENTS_ENDPOINTS.withdraw; + + const upstreamRes = await fetch(getPaymentsApiUrl(endpoint), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + owner, + ...(PAYMENTS_CLUSTER ? { cluster: PAYMENTS_CLUSTER } : {}), + mint, + amount: Number(amountBigInt), + initIfMissing: true, + initAtasIfMissing: true, + ...(mode === "shield" ? { initVaultIfMissing: true } : {}), + idempotent: true, + }), + signal: getPaymentsTimeoutSignal(), + cache: "no-store", + }); + + const responseBody = await upstreamRes.json().catch(() => null); + if (!upstreamRes.ok) { + return NextResponse.json( + { + error: getPaymentsErrorMessage(upstreamRes.status, responseBody), + details: responseBody, + }, + { status: upstreamRes.status } + ); + } + + return NextResponse.json(responseBody); + } catch (error) { + console.error("Payments shield build error:", error); + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Failed to build shield transaction", + }, + { status: 500 } + ); + } +} diff --git a/app/api/payments/transaction/send/route.ts b/app/api/payments/transaction/send/route.ts new file mode 100644 index 0000000..8bf69be --- /dev/null +++ b/app/api/payments/transaction/send/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + PAYMENTS_CLUSTER, + PAYMENTS_ENDPOINTS, + getPaymentsApiUrl, + getPaymentsTimeoutSignal, +} from "@/lib/payments"; +import { getPaymentsErrorMessage } from "@/lib/payments-errors"; + +interface PaymentTransactionSendRequest { + transactionBase64?: string; + sendTo?: "base" | "ephemeral"; +} + +export async function POST(request: NextRequest) { + try { + const body = (await request.json()) as PaymentTransactionSendRequest; + const { + transactionBase64, + sendTo, + } = body; + const authorization = request.headers.get("authorization")?.trim() ?? ""; + + if ( + typeof transactionBase64 !== "string" || + !transactionBase64.trim() || + (sendTo !== "base" && sendTo !== "ephemeral") + ) { + return NextResponse.json( + { error: "Missing or invalid transaction send parameters" }, + { status: 400 } + ); + } + + if (sendTo === "ephemeral" && !authorization) { + return NextResponse.json( + { error: "Shielded transaction submission requires authentication" }, + { status: 400 } + ); + } + + const upstreamHeaders: HeadersInit = { + "Content-Type": "application/json", + }; + if (authorization) { + upstreamHeaders.Authorization = authorization; + } + + const upstreamRes = await fetch( + getPaymentsApiUrl(PAYMENTS_ENDPOINTS.transactionSend), + { + method: "POST", + headers: upstreamHeaders, + body: JSON.stringify({ + transactionBase64, + sendTo, + ...(PAYMENTS_CLUSTER ? { cluster: PAYMENTS_CLUSTER } : {}), + }), + signal: getPaymentsTimeoutSignal(30_000), + cache: "no-store", + } + ); + + const responseBody = await upstreamRes.json().catch(() => null); + if (!upstreamRes.ok) { + return NextResponse.json( + { + error: getPaymentsErrorMessage(upstreamRes.status, responseBody), + details: responseBody, + }, + { status: upstreamRes.status } + ); + } + + return NextResponse.json(responseBody); + } catch (error) { + console.error("Payments transaction send error:", error); + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Failed to send payment transaction", + }, + { status: 500 } + ); + } +} diff --git a/app/api/payments/transfer/route.ts b/app/api/payments/transfer/route.ts index 2281d58..0e55535 100644 --- a/app/api/payments/transfer/route.ts +++ b/app/api/payments/transfer/route.ts @@ -13,6 +13,9 @@ interface PaymentTransferBuildRequest { mint?: string; amount?: string; visibility?: "public" | "private"; + fromBalance?: "base" | "ephemeral"; + toBalance?: "base" | "ephemeral"; + authToken?: string; gasless?: boolean; memo?: string; exactOut?: boolean; @@ -32,6 +35,9 @@ export async function POST(request: NextRequest) { mint, amount, visibility, + fromBalance, + toBalance, + authToken, gasless, memo, exactOut, @@ -45,6 +51,7 @@ export async function POST(request: NextRequest) { typeof to !== "string" || typeof mint !== "string" || typeof amount !== "string" || + (authToken !== undefined && typeof authToken !== "string") || (gasless !== undefined && typeof gasless !== "boolean") || (memo !== undefined && typeof memo !== "string") || (exactOut !== undefined && typeof exactOut !== "boolean") || @@ -54,6 +61,12 @@ export async function POST(request: NextRequest) { (typeof maxDelayMs !== "string" || !/^\d+$/.test(maxDelayMs))) || (split !== undefined && (!Number.isInteger(split) || split < 1 || split > 10)) || + (fromBalance !== undefined && + fromBalance !== "base" && + fromBalance !== "ephemeral") || + (toBalance !== undefined && + toBalance !== "base" && + toBalance !== "ephemeral") || (visibility !== "public" && visibility !== "private") ) { return NextResponse.json( @@ -109,9 +122,33 @@ export async function POST(request: NextRequest) { ); } + const resolvedFromBalance = fromBalance ?? "base"; + const resolvedToBalance = toBalance ?? "base"; + + if (resolvedToBalance === "ephemeral" && visibility !== "private") { + return NextResponse.json( + { error: "Shielded balance delivery requires shielded routing" }, + { status: 400 } + ); + } + + if (resolvedFromBalance === "ephemeral" && !authToken?.trim()) { + return NextResponse.json( + { error: "Shielded balance source requires authentication" }, + { status: 400 } + ); + } + + const upstreamHeaders: HeadersInit = { + "Content-Type": "application/json", + }; + if (resolvedFromBalance === "ephemeral") { + upstreamHeaders.Authorization = `Bearer ${authToken?.trim()}`; + } + const upstreamRes = await fetch(getPaymentsApiUrl(PAYMENTS_ENDPOINTS.splTransfer), { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: upstreamHeaders, body: JSON.stringify({ from, to, @@ -119,11 +156,11 @@ export async function POST(request: NextRequest) { mint, amount: Number(amountBigInt), visibility, - fromBalance: "base", - toBalance: "base", + fromBalance: resolvedFromBalance, + toBalance: resolvedToBalance, initIfMissing: true, initAtasIfMissing: true, - initVaultIfMissing: false, + initVaultIfMissing: resolvedToBalance === "ephemeral", ...(gasless === true ? { gasless: true } : {}), ...(memo ? { memo } : {}), ...(exactOut !== undefined ? { exactOut } : {}), diff --git a/app/api/swap/quote/route.ts b/app/api/swap/quote/route.ts index 2565a3d..0433e71 100644 --- a/app/api/swap/quote/route.ts +++ b/app/api/swap/quote/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { PAYMENTS_ENDPOINTS, + PAYMENTS_CLUSTER, getPaymentsApiUrl, getPaymentsTimeoutSignal, } from "@/lib/payments"; @@ -12,6 +13,13 @@ import { * slippageBps (default 50 = 0.5%) */ export async function GET(request: NextRequest) { + if (PAYMENTS_CLUSTER === "devnet") { + return NextResponse.json( + { error: "Swap is disabled on devnet" }, + { status: 403 } + ); + } + const { searchParams } = request.nextUrl; const inputMint = searchParams.get("inputMint"); const outputMint = searchParams.get("outputMint"); diff --git a/app/api/swap/route.ts b/app/api/swap/route.ts index 94cfaa7..af9820d 100644 --- a/app/api/swap/route.ts +++ b/app/api/swap/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { PublicKey } from "@solana/web3.js"; import { PAYMENTS_ENDPOINTS, + PAYMENTS_CLUSTER, getPaymentsApiUrl, getPaymentsTimeoutSignal, } from "@/lib/payments"; @@ -35,6 +36,13 @@ interface SwapBuildRequest { */ export async function POST(request: NextRequest) { try { + if (PAYMENTS_CLUSTER === "devnet") { + return NextResponse.json( + { error: "Swap is disabled on devnet" }, + { status: 403 } + ); + } + const body = (await request.json()) as SwapBuildRequest; const { quoteResponse, @@ -87,7 +95,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { error: - "Private swaps require destination, minDelayMs, maxDelayMs, and split", + "Shielded swaps require destination, minDelayMs, maxDelayMs, and split", }, { status: 400 } ); diff --git a/app/page.tsx b/app/page.tsx index 50b4883..9b6ac68 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,6 +2,7 @@ import { Header } from "@/components/one/header"; import { TradeHub } from "@/components/one/trade-hub"; // import { TokenPrices } from "@/components/one/token-prices"; import { NetWorthPanel } from "@/components/one/net-worth-panel"; +import { PAYMENTS_CLUSTER } from "@/lib/payments"; type HomeProps = { searchParams: Promise<{ @@ -56,19 +57,20 @@ export default async function Home({ searchParams }: HomeProps) {
- -
{/* Subtitle */}

Onchain Payment Made Simple

+ + {/* Swap / Payment Section */} {/* Token Prices */} diff --git a/app/wallet/solana-wallet-provider.tsx b/app/wallet/solana-wallet-provider.tsx index ca53149..74083d9 100644 --- a/app/wallet/solana-wallet-provider.tsx +++ b/app/wallet/solana-wallet-provider.tsx @@ -28,12 +28,14 @@ import { Transaction, VersionedTransaction, } from "@solana/web3.js"; +import { getBase58Decoder } from "@solana/codecs-strings"; +import { createSolanaRpc } from "@solana/rpc"; +import { createSolanaRpcSubscriptions } from "@solana/rpc-subscriptions"; import { - createSolanaRpc, - createSolanaRpcSubscriptions, - getBase58Decoder, -} from "@solana/kit"; -import { PrivyProvider, usePrivy } from "@privy-io/react-auth"; + PrivyProvider, + usePrivy, + type PrivyClientConfig, +} from "@privy-io/react-auth"; import { useWallets as usePrivyWallets } from "@privy-io/react-auth/solana"; import { SOLANA_PUBLIC_RPC_ENDPOINT } from "@/lib/solana-rpc"; import { @@ -521,17 +523,18 @@ export function SolanaWalletProvider({ children }: { children: ReactNode }) { [endpoint], ); const privySolanaConfig = useMemo( - () => ({ - rpcs: { - [privySolanaChain]: { - rpc: createSolanaRpc(endpoint), - rpcSubscriptions: createSolanaRpcSubscriptions( - getSolanaWsEndpoint(endpoint), - ), - blockExplorerUrl: getSolanaExplorerUrl(privySolanaChain), + () => + ({ + rpcs: { + [privySolanaChain]: { + rpc: createSolanaRpc(endpoint), + rpcSubscriptions: createSolanaRpcSubscriptions( + getSolanaWsEndpoint(endpoint), + ), + blockExplorerUrl: getSolanaExplorerUrl(privySolanaChain), + }, }, - }, - }), + }) as unknown as NonNullable, [endpoint, privySolanaChain], ); const wallets = useMemo( diff --git a/components/one/header.tsx b/components/one/header.tsx index d219929..386cc6d 100644 --- a/components/one/header.tsx +++ b/components/one/header.tsx @@ -43,17 +43,17 @@ import { const FAQ_ITEMS = [ { value: "item-0", - question: "Is MagicBlock Private Payment API a mixer?", + question: "Is MagicBlock Shielded Payment API a mixer?", answer: ( <>

No.

- MagicBlock Private Payment API is not a mixer and does not rely on pooling or + MagicBlock Shielded Payment API is not a mixer and does not rely on pooling or redistributing user funds to obscure ownership.

Funds are first locked in a vault on Solana, and release to the - recipient is authorized through a private intent executed inside + recipient is authorized through a shielded intent executed inside MagicBlock's ephemeral rollup. This design is intended to obscure the direct on-chain link between sender and recipient while supporting compliance through permissioned access, policy enforcement, and AML / @@ -64,17 +64,17 @@ const FAQ_ITEMS = [ }, { value: "item-1", - question: "Are private payments truly private?", + question: "Are shielded payments truly shielded?", answer: ( <>

- MagicBlock private payments are designed to provide strong privacy, + MagicBlock shielded payments are designed to provide strong privacy, but not absolute anonymity.

The system obscures the direct on-chain link between sender and recipient by separating deposit and payout flows and executing - transaction logic privately. However, observers may still see funds + transaction logic in a shielded environment. However, observers may still see funds entering and exiting the system on Solana and could attempt statistical correlation.

@@ -106,7 +106,7 @@ const FAQ_ITEMS = [ question: "How does MagicBlock support compliance?", answer: ( <> -

MagicBlock Private Payment API is designed to support compliance through:

+

MagicBlock Shielded Payment API is designed to support compliance through: