Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e9ea639
added starknet Sepolia to noblocks
Dprof-in-tech Dec 23, 2025
7adb884
feat: migrate from Starknet Sepolia to Starknet Mainnet across the ap…
Dprof-in-tech Jan 5, 2026
0d0e5a8
refactor: remove unused import of 'hash' from crypto in create-order …
Dprof-in-tech Jan 5, 2026
89855e4
refactor: remove console logs from create-order route and update Star…
Dprof-in-tech Jan 7, 2026
e4dabf4
enhanced starknet integration on noblocks
Dprof-in-tech Jan 8, 2026
b0c44d9
feat: enhance Starknet wallet deployment and transaction handling acr…
Dprof-in-tech Jan 8, 2026
660a45b
fix: correct user verification logic in swap button hook
Dprof-in-tech Jan 8, 2026
ef5f992
fix: update balance selection logic for Starknet network
Dprof-in-tech Jan 8, 2026
da12036
Merge branch 'main' of https://github.com/paycrest/noblocks into feat…
Dprof-in-tech Jan 8, 2026
4e89666
refactor: update public key variable name and improve error handling …
Dprof-in-tech Jan 8, 2026
3be71f7
Merge branch 'main' into feat-starknet-on-noblocks
Dprof-in-tech Jan 16, 2026
155aa25
feat: enhance Starknet wallet functionality and user authorization ch…
Dprof-in-tech Jan 16, 2026
348d8ab
feat: add authentication check for wallet creation and state retrieval
Dprof-in-tech Jan 16, 2026
27b5e8d
fix: update USDC token address in fallback tokens
Dprof-in-tech Jan 16, 2026
3d327d6
fix: correct transaction hash property name in tracking event
Dprof-in-tech Jan 16, 2026
b68ed57
fix: update wallet address assignment to include starknet address fal…
Dprof-in-tech Jan 16, 2026
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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ NEXT_PUBLIC_BLOCKFEST_END_DATE=2025-10-11T23:59:00+01:00
CASHBACK_WALLET_ADDRESS=
CASHBACK_WALLET_PRIVATE_KEY=

# =============================================================================
# Starknet Configuration
# =============================================================================

# Starknet Network RPC
NEXT_PUBLIC_STARKNET_RPC_URL=https://starknet-mainnet.public.blastapi.io

# Ready Account Class Hash (Argent Ready Account)
NEXT_PUBLIC_STARKNET_READY_CLASSHASH=0x073414441639dcd11d1846f287650a00c60c416b9d3ba45d31c651672125b2c2
STARKNET_READY_CLASSHASH=0x073414441639dcd11d1846f287650a00c60c416b9d3ba45d31c651672125b2c2

# Paymaster Configuration (AVNU)
STARKNET_PAYMASTER_URL=https://mainnet.paymaster.avnu.fi
STARKNET_PAYMASTER_MODE=sponsored
STARKNET_PAYMASTER_API_KEY=

# Gas token address (required if PAYMASTER_MODE=default)
STARKNET_GAS_TOKEN_ADDRESS=

# =============================================================================
# Content Management (Sanity)
# =============================================================================
Expand Down
288 changes: 288 additions & 0 deletions app/api/starknet/create-order/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/app/lib/jwt";
import { DEFAULT_PRIVY_CONFIG } from "@/app/lib/config";
import {
buildReadyAccount,
deployReadyAccount,
getRpcProvider,
getStarknetWallet,
setupPaymaster,
} from "@/app/lib/starknet";
import { cairo, CallData, byteArray } from "starknet";

export async function POST(request: NextRequest) {
let isDeployed = false;
try {
// Extract and verify JWT token
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Missing or invalid authorization header" },
{ status: 401 },
);
}

const token = authHeader.substring(7);
const { payload } = await verifyJWT(token, DEFAULT_PRIVY_CONFIG);
const authUserId = payload.sub || payload.userId;

if (!authUserId) {
return NextResponse.json(
{ error: "Invalid token: missing user ID" },
{ status: 401 },
);
}

// Get request body
const body = await request.json();
const {
walletId,
publicKey,
classHash: clientClassHash,
tokenAddress,
gatewayAddress,
amount,
rate,
senderFeeRecipient,
senderFee,
refundAddress,
messageHash,
origin: clientOrigin,
address: WalletAddress,
} = body;

const provider = getRpcProvider();
try {
await provider.getClassHashAt(WalletAddress);
isDeployed = true;
} catch {
isDeployed = false;
}

// Validate required fields
if (!walletId || !publicKey || !tokenAddress || !gatewayAddress) {
return NextResponse.json(
{
error: "Missing required fields",
missing: {
walletId: !walletId,
publicKey: !publicKey,
tokenAddress: !tokenAddress,
gatewayAddress: !gatewayAddress,
},
},
{ status: 400 },
);
}

if (
amount === undefined ||
rate === undefined ||
!senderFeeRecipient ||
senderFee === undefined ||
!refundAddress ||
!messageHash
) {
return NextResponse.json(
{
error: "Missing transaction parameters",
missing: {
amount: amount === undefined,
rate: rate === undefined,
senderFeeRecipient: !senderFeeRecipient,
senderFee: senderFee === undefined,
refundAddress: !refundAddress,
messageHash: !messageHash,
},
},
{ status: 400 },
);
}

// Use class hash from client or fallback to server env
const classHash = clientClassHash || process.env.STARKNET_READY_CLASSHASH;
if (!classHash) {
return NextResponse.json(
{ error: "STARKNET_READY_CLASSHASH not configured" },
{ status: 500 },
);
}

// Use origin from client or header
const origin = clientOrigin || request.headers.get("origin") || undefined;

const { publicKey: walletPublicKey } = await getStarknetWallet(walletId);

// Setup paymaster if configured
const usePaymaster = !!(
process.env.STARKNET_PAYMASTER_URL && process.env.STARKNET_PAYMASTER_MODE
);

if (!usePaymaster) {
return NextResponse.json(
{ error: "Paymaster not configured" },
{ status: 500 },
);
}

let config;
try {
config = await setupPaymaster();
} catch (e: any) {
return NextResponse.json(
{ error: e?.message || "Failed to initialize paymaster" },
{ status: 500 },
);
}

const { paymasterRpc, isSponsored, gasToken } = config;

// Build account with paymaster support
const { account, address } = await buildReadyAccount({
walletId,
publicKey: walletPublicKey,
classHash,
userJwt: token,
userId: authUserId,
origin,
paymasterRpc,
});

// Convert amounts to u256 (following the working script pattern)
const amountU256 = cairo.uint256(BigInt(amount));
const senderFeeU256 = cairo.uint256(BigInt(senderFee));

// Encode message hash as Cairo ByteArray
const messageHashByteArray = byteArray.byteArrayFromString(messageHash);

// Calculate total amount (amount + senderFee)
const totalAmount = BigInt(amount) + BigInt(senderFee);
const totalAmountU256 = cairo.uint256(totalAmount);

// Prepare calls using manual call structure (more reliable than populate)
const calls = [
// 1. Approve gateway to spend tokens
{
contractAddress: tokenAddress,
entrypoint: "approve",
calldata: CallData.compile({
spender: gatewayAddress,
amount: totalAmountU256,
}),
},
// 2. Create order
{
contractAddress: gatewayAddress,
entrypoint: "create_order",
calldata: CallData.compile({
token: tokenAddress,
amount: amountU256,
rate: rate,
sender_fee_recipient: senderFeeRecipient,
sender_fee: senderFeeU256,
refund_address: refundAddress,
message_hash: messageHashByteArray, // Use encoded ByteArray
}),
},
];

// Prepare paymaster details
const paymasterDetails: any = isSponsored
? { feeMode: { mode: "sponsored" as const } }
: { feeMode: { mode: "default" as const, gasToken } };

// Estimate fees if not sponsored
let maxFee: any = undefined;
if (!isSponsored) {
try {
const est = await account.estimatePaymasterTransactionFee(
calls,
paymasterDetails,
);
const withMargin15 = (v: any) => {
const bi = BigInt(v.toString());
return (bi * BigInt(3) + BigInt(1)) / BigInt(2); // ceil(1.5x)
};
maxFee = withMargin15(est.suggested_max_fee_in_gas_token);
} catch (error: any) {
console.error("[API] Fee estimation failed:", error.message);
return NextResponse.json(
{ error: `Fee estimation failed: ${error.message}` },
{ status: 500 },
);
}
}

// Execute transaction with paymaster
let result;
try {
if (!isDeployed) {
result = await deployReadyAccount({
walletId,
publicKey: walletPublicKey,
classHash,
userJwt: token,
userId: authUserId,
origin,
calls,
});
} else {
result = await account.executePaymasterTransaction(
calls,
paymasterDetails,
maxFee,
);
}
} catch (error: any) {
console.error("[API] Error executing transaction:", error);
return NextResponse.json(
{ error: error.message || "Failed to execute transaction" },
{ status: 500 },
);
}

let orderId;

// Wait for transaction confirmation
const wait = true;
if (wait) {
try {
const txReceipt = await account.waitForTransaction(
result.transaction_hash,
);
if (txReceipt.isSuccess()) {
const rawEvents = txReceipt.value.events;
rawEvents.forEach((event) => {
if (
Object.values(event.keys).includes(
"0x3427759bfd3b941f14e687e129519da3c9b0046c5b9aaa290bb1dede63753b3",
)
) {
orderId = event.data[2];
}
});
}
} catch (error) {
console.log(
"[API] Warning: Could not confirm transaction, but it may still succeed",
);
}
}

return NextResponse.json({
success: true,
walletId,
address,
transactionHash: result.transaction_hash,
mode: isSponsored ? "sponsored" : "default",
messageHash,
orderId,
});
} catch (error: any) {
console.error("[API] Error executing transaction:", error);
return NextResponse.json(
{ error: error.message || "Failed to execute transaction" },
{ status: 500 },
);
}
}
Loading