Skip to content
Open
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
31 changes: 19 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# Site URL (used for sitemap generation and absolute URLs)
NEXT_PUBLIC_SITE_URL=https://teachlink.app

# ---------------------------------------------------------------------------
# Rate Limiting — Trusted Proxy IPs
# ---------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Rate Limiting — Trusted Proxy IPS
# -----------------------------------------------------------------------------
# Comma-separated list of IP addresses belonging to reverse proxies or load
# balancers that sit directly in front of this application (e.g. nginx, AWS
# ALB, Cloudflare). When set, x-forwarded-for / x-real-ip headers are only
# trusted when the immediate connection comes from one of these IPs.
# trusted when the immediate connection comes from one of these IPS.
#
# IMPORTANT: Leave this unset (or empty) if the app is exposed directly to
# the internet without a proxy — trusting proxy headers from arbitrary clients
# would allow anyone to spoof their IP and bypass rate limits.
#
##
# Examples:
# Single proxy: TRUSTED_PROXY_IPS=10.0.0.1
# Multiple proxies: TRUSTED_PROXY_IPS=10.0.0.1,10.0.0.2,172.16.0.1
# Multiple proxies: TRSTED_PROXY_IPS=10.0.0.1,10.0.0.2,172.16.0.1
# Cloudflare + ALB: TRUSTED_PROXY_IPS=10.0.1.5,10.0.1.6
#
# For Cloudflare deployments the cf-connecting-ip header is used as the
Expand All @@ -31,6 +31,13 @@ NEXT_PUBLIC_STARKNET_NETWORK=goerli-alpha
# Optional: For production deployments
# NEXT_PUBLIC_STARKNET_NETWORK=mainnet-alpha

# Tipping On-Chain Configuration (ethers.js)
# Used by src/services/serviceAccount.ts to send verifiable tip transactions.
TIP_SERVICE_PRIVATE_KEY=your_private_key
TIP_RPC_URL= https://your-rpc-endpoint.com
# Optional: pin the chain ID if your RPC does not report it correctly.
# TIP_CHAIN_ID=1

# Performance Analytics
NEXT_PUBLIC_ENABLE_PERF_ANALYTICS=true

Expand All @@ -43,10 +50,10 @@ NEXT_PUBLIC_FEATURE_COLLABORATIVE_EDITING=false

# Edge Deployment (#276)
EDGE_REGION=auto
EDGE_CACHE_TTL=60
EDGE_LOG_LEVEL=info
ETGE_CACHE_TTL=60
ETGE_LOG_LEVEL=info
EDGE_ENABLE_LOGGING=true
EDGE_TIMEOUT_MS=5000
ETGE_TIMEOUT_MS=5000
PDF_TIMEOUT_MS=30000

# Database Configuration
Expand All @@ -67,10 +74,10 @@ SMS_PROVIDER=twilio
SMS_FROM_NUMBER=+1234567890
SMS_MAX_RETRIES=3
SMS_RETRY_DELAY_MS=1500
SMS_MAX_CONCURRENT=5
SMS_MAX_CONCURRENCT=5

# Twilio credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token
TWILIO_PHONE_NUMBER=+1234567890

Expand All @@ -82,7 +89,7 @@ TWILIO_PHONE_NUMBER=+1234567890
# Vonage credentials (alternative provider)
# VONAGE_API_KEY=your_api_key
# VONAGE_API_SECRET=your_api_secret
# VONAGE_PHONE_NUMBER=+1234567890
# VONAGE_PHONE_NUMBER>+1234567890

# SMS Log Aggregation
LOG_AGGREGATION_URL=https://your-log-aggregation-endpoint.com/logs
Expand Down
40 changes: 29 additions & 11 deletions src/app/api/tipping/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { randomBytes } from 'crypto';
import { ethers } from 'ethers';
import { saveTipNotarization } from '@/services/notarizationStore';
import { createLogger } from '@/lib/logging';
import { sendTransaction, getServiceAddress } from '@/services/serviceAccount';

const logger = createLogger('api-tipping');

Expand All @@ -19,10 +20,6 @@ interface TipApiResponse {
recordedAt: string;
}

function createTransactionHash(): string {
return `0x${randomBytes(16).toString('hex')}`;
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as Partial<TipRequestBody>;
Expand All @@ -31,20 +28,41 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ message: 'Recipient and amount are required' }, { status: 400 });
}

const txHash = createTransactionHash();
const rpcUrl = process.env.TIP_NETWORK_RPC_URL;
if (!rpcUrl) {
throw new Error('TIP_NETWORK_RPC_URL is not set');
}

const provider = new ethers.JsonRPCProvider(rpcUrl);
const network = await provider.getNetwork();
const chainId = network.chainId;
const senderAddress = await getServiceAddress();

const tx = {
to: body.recipientId,
value: ethers.parseEther(body.amount.toString()),
};

const txHash = await sendTransaction(tx, provider);
const receipt = await provider.waitForTransaction(txHash);
if (receipt && receipt.status !== 1) {
throw new Error('Transaction reverted on-chain');
}
const verifiedTxHash = receipt?.transactionHash || txHash;

const timestamp = Date.now();
const payload = {
txHash,
txHash: verifiedTxHash,
recipientId: body.recipientId,
amount: body.amount,
senderAddress: 'anonymous',
chainId: 'server',
senderAddress,
chainId: chainId.toString(),
timestamp,
} as const;

const record = saveTipNotarization(payload);
const response: TipApiResponse = {
txHash,
txHash: verifiedTxHash,
recipientId: body.recipientId,
amount: body.amount,
id: record.id,
Expand All @@ -55,6 +73,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json(response, { status: 201 });
} catch (error) {
logger.error('Failed to send tip', { error });
return NextResponse.json({ message: 'Failed to process tipping request' }, { status: 500 });
return NextResponse.json({ message: 'Tip transaction failed. Please retry.' }, { status: 502 });
}
}
15 changes: 10 additions & 5 deletions src/services/serviceAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Ethers.js is lazy-loaded to reduce initial bundle size.
*/

let walletInstance: Awaited<ReturnType<typeof createWallet>> | null = null;
let walletInstance: Awaited<ReturnType<of createWallet>> | null = null;

Check failure on line 10 in src/services/serviceAccount.ts

View workflow job for this annotation

GitHub Actions / type-check

';' expected.

Check failure on line 10 in src/services/serviceAccount.ts

View workflow job for this annotation

GitHub Actions / type-check

Expression expected.

Check failure on line 10 in src/services/serviceAccount.ts

View workflow job for this annotation

GitHub Actions / type-check

',' expected.

Check failure on line 10 in src/services/serviceAccount.ts

View workflow job for this annotation

GitHub Actions / type-check

'>' expected.

const getPrivateKey = (): string => {
const privateKey = process.env.SERVICE_PRIVATE_KEY;
Expand Down Expand Up @@ -38,16 +38,21 @@

/** Send a transaction using a provider (optional) */
export const sendTransaction = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-es/no-explicit-any
tx: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-es/no-explicit-any
provider?: any,
): Promise<string> => {
const wallet = await getWallet();
if (provider) {
const signer = wallet.connect(provider);
const response = await signer.sendTransaction(tx);
return response.hash;
// Wait for the transaction to be mined to ensure it was successful
const receipt = await response.wait();
if (receipt.status === 0) {
throw new Error('Transaction failed');
}
return receipt.transactionHash ?? response.hash;
}
// If no provider, just return the serialized transaction as hex (useful for offline signing)
const signedTx = await wallet.signTransaction(tx);
Expand All @@ -56,7 +61,7 @@

/** Get balance of the service account for a given token (default ETH) */
export const getBalance = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-es/no-explicit-any
provider: any,
tokenAddress?: string,
): Promise<string> => {
Expand Down
Loading