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
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Soroban RPC Settings
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
SOROBAN_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
# Anchor API variables
ANCHOR_API_BASE_URL=https://api.example.com/anchor

# Session encryption for wallet-based auth (required for /api/auth/*).
# Generate with: openssl rand -base64 32
SESSION_PASSWORD=your-32-char-minimum-secret-here
ANCHOR_WEBHOOK_SECRET="your_shared_anchor_secret_here"

# Shared secret used to verify Anchor webhooks
ANCHOR_WEBHOOK_SECRET=your_shared_anchor_secret_here
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ See [API Routes Documentation](./docs/API_ROUTES.md) for details on authenticati
- Integrate with anchor platform APIs for fiat on/off-ramps
- Handle deposit/withdrawal flows
- Process exchange rate quotes
- **Environment Setup:** Set `ANCHOR_API_BASE_URL` in your `.env` file (see `.env.example`).
- **Frontend API Endpoint:** Use `GET /api/anchor/rates` to fetch cached exchange rates with fallback support.

4. **Transaction Tracking**
- Display on-chain transaction history
Expand Down
63 changes: 63 additions & 0 deletions app/api/anchor/rates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { NextResponse } from 'next/server';
import { anchorClient, ExchangeRate } from '@/lib/anchor/client';

export const dynamic = 'force-dynamic';

interface CacheData {
rates: ExchangeRate[] | null;
timestamp: number;
}

// In-memory cache variables for rates.
// Next.js development server may drop this cache on hot-reload, but it will work effectively in a production environment (serverless instance lifetime).
let rateCache: CacheData = {
rates: null,
timestamp: 0,
};

// 5 minutes in milliseconds
const CACHE_TTL = 5 * 60 * 1000;

export async function GET() {
const now = Date.now();
const isCacheValid = rateCache.rates !== null && (now - rateCache.timestamp) < CACHE_TTL;

if (isCacheValid) {
return NextResponse.json({
rates: rateCache.rates,
stale: false,
});
}

try {
const fetchedRates = await anchorClient.getExchangeRates();

// Update the cache
rateCache = {
rates: fetchedRates,
timestamp: now,
};

return NextResponse.json({
rates: fetchedRates,
stale: false,
});
} catch (error) {
console.error('API /anchor/rates - Error fetching from Anchor Client:', error);

// Fallback: If cache exists but is stale, return the stale cache.
if (rateCache.rates !== null) {
console.warn('API /anchor/rates - Returning stale rate cache due to anchor failure.');
return NextResponse.json({
rates: rateCache.rates,
stale: true,
});
}

// No cache exists and the fetch failed
return NextResponse.json(
{ error: 'Service Unavailable' },
{ status: 503 }
);
}
}
2 changes: 1 addition & 1 deletion app/dashboard/insight/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import SixMonthTrendsWidget from '@/components/dashboard/SixMonthTrendsWidget'
import SixMonthTrendsWidget from '@/components/Dashboard/SixMonthTrendsWidget'
export default function InsightPage() {
return (
<div
Expand Down
18 changes: 6 additions & 12 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,28 @@ export default function Dashboard() {
<StatCard
title="Total Sent"
value="$1,200"
detail1="+$300"
detail1Color="text-red-500"
detail2="+25%"
percentage="+25%"
icon={<Send className="w-5 h-5" />}
showTrend={true}
/>
<StatCard
title="Savings"
value="$360"
detail1="+$90"
detail1Color="text-red-500"
detail2="+33%"
percentage="+33%"
icon={<PiggyBank className="w-5 h-5" />}
showTrend={true}
/>
<StatCard
title="Bills Paid"
value="$180"
detail1="3 bills"
detail2="This month"
percentage="0%"
icon={<FileText className="w-5 h-5" />}
trend="none"
/>
<StatCard
title="Insurance"
value="$60"
detail1="2 policies"
detail2="Active"
percentage="0%"
icon={<Shield className="w-5 h-5" />}
trend="none"
/>
</div>

Expand Down
112 changes: 112 additions & 0 deletions lib/anchor/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
export interface ExchangeRate {
sell_asset: string;
buy_asset: string;
price: string;
}

export interface QuoteRequest {
from: string;
to: string;
amount: string;
}

export interface QuoteResponse {
price: string;
sell_amount: string;
buy_amount: string;
fee: {
total: string;
asset: string;
};
}

const DEFAULT_TIMEOUT_MS = 5000;

export class AnchorClient {
private baseUrl: string;

constructor() {
this.baseUrl = process.env.ANCHOR_API_BASE_URL || '';
if (!this.baseUrl) {
console.warn('ANCHOR_API_BASE_URL is not set. Anchor API calls may fail.');
}
}

/**
* Helper method to perform fetch with timeout
*/
private async fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs: number = DEFAULT_TIMEOUT_MS): Promise<Response> {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);

try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
} catch (error: unknown) {
clearTimeout(id);
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}

/**
* Fetches the current exchange rates from the Anchor API.
* Based on SEP-38: /info or /prices depending on the implementation.
* Assuming a simplified /rates endpoint for this client.
*/
async getExchangeRates(): Promise<ExchangeRate[]> {
if (!this.baseUrl) throw new Error('Anchor Base URL not configured');

const url = `${this.baseUrl}/rates`;

try {
const response = await this.fetchWithTimeout(url);

if (!response.ok) {
throw new Error(`Failed to fetch rates: HTTP ${response.status}`);
}

// We expect the anchor to return an array of rates or an object containing an array.
// Adjust according to the actual anchor API specification.
const data = await response.json();
return data.rates || data;
} catch (error) {
console.error('AnchorClient: Error fetching exchange rates:', error);
throw error;
}
}

/**
* Fetches a quote for a specific pair and amount.
*/
async getQuote({ from, to, amount }: QuoteRequest): Promise<QuoteResponse> {
if (!this.baseUrl) throw new Error('Anchor Base URL not configured');

const url = new URL(`${this.baseUrl}/quote`);
url.searchParams.append('sell_asset', from);
url.searchParams.append('buy_asset', to);
url.searchParams.append('sell_amount', amount);

try {
const response = await this.fetchWithTimeout(url.toString());

if (!response.ok) {
throw new Error(`Failed to fetch quote: HTTP ${response.status}`);
}

return await response.json();
} catch (error) {
console.error('AnchorClient: Error fetching quote:', error);
throw error;
}
}
}

// Export a singleton instance for direct utility usage
export const anchorClient = new AnchorClient();
Loading
Loading