Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .snyk
Original file line number Diff line number Diff line change
@@ -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: {}
7 changes: 6 additions & 1 deletion app/api/explorer/tx/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -11,5 +14,7 @@ export async function GET(request: NextRequest) {
);
}

return NextResponse.redirect(getPaymentsExplorerTransactionUrl(signature));
return NextResponse.redirect(
getPaymentsExplorerTransactionUrl(signature, customRpcEndpoint)
);
}
106 changes: 106 additions & 0 deletions app/api/payments/shield/route.ts
Original file line number Diff line number Diff line change
@@ -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({
Comment thread
GabrielePicco marked this conversation as resolved.
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 }
);
}
}
88 changes: 88 additions & 0 deletions app/api/payments/transaction/send/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
Comment thread
GabrielePicco marked this conversation as resolved.
);
}

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 }
);
}
}
45 changes: 41 additions & 4 deletions app/api/payments/transfer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +35,9 @@ export async function POST(request: NextRequest) {
mint,
amount,
visibility,
fromBalance,
toBalance,
authToken,
gasless,
memo,
exactOut,
Expand All @@ -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") ||
Expand All @@ -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(
Expand Down Expand Up @@ -109,21 +122,45 @@ 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,
...(PAYMENTS_CLUSTER ? { cluster: PAYMENTS_CLUSTER } : {}),
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 } : {}),
Expand Down
8 changes: 8 additions & 0 deletions app/api/swap/quote/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import {
PAYMENTS_ENDPOINTS,
PAYMENTS_CLUSTER,
getPaymentsApiUrl,
getPaymentsTimeoutSignal,
} from "@/lib/payments";
Expand All @@ -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");
Expand Down
10 changes: 9 additions & 1 deletion app/api/swap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
);
Expand Down
6 changes: 4 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -56,19 +57,20 @@ export default async function Home({ searchParams }: HomeProps) {
<Header />

<main className="flex-1 relative z-10">
<NetWorthPanel />

<div className="flex flex-col items-center px-4 pt-4 pb-10">
{/* Subtitle */}
<p className="text-sm text-muted-foreground mb-4">
Onchain Payment Made Simple
</p>

<NetWorthPanel />

{/* Swap / Payment Section */}
<TradeHub
initialBuyMint={initialBuyMint}
initialSellMint={initialSellMint}
initialSwapAmount={initialSwapAmount}
isSwapDisabled={PAYMENTS_CLUSTER === "devnet"}
/>

{/* Token Prices */}
Expand Down
Loading
Loading