mirror of
https://github.com/pezkuwichain/pezkuwi-p2p-mobile.git
synced 2026-04-22 01:57:56 +00:00
feat: use Edge Functions for P2P operations with Telegram auth
- createFiatOffer: Uses create-offer-telegram Edge Function - AdList: Uses get-my-offers Edge Function for "my-ads" tab - Added adType parameter to CreateOfferParams Fixes RLS issues where Telegram auth doesn't set auth.uid().
This commit is contained in:
@@ -51,57 +51,77 @@ export function AdList({ type, filters }: AdListProps) {
|
||||
try {
|
||||
let offersData: P2PFiatOffer[] = [];
|
||||
|
||||
// Build base query
|
||||
let query = supabase.from('p2p_fiat_offers').select('*');
|
||||
|
||||
if (type === 'buy') {
|
||||
// Buy tab = show SELL offers (user wants to buy from sellers)
|
||||
query = query.eq('ad_type', 'sell').eq('status', 'open').gt('remaining_amount', 0);
|
||||
} else if (type === 'sell') {
|
||||
// Sell tab = show BUY offers (user wants to sell to buyers)
|
||||
query = query.eq('ad_type', 'buy').eq('status', 'open').gt('remaining_amount', 0);
|
||||
} else if (type === 'my-ads' && user) {
|
||||
// My offers - show all of user's offers
|
||||
query = query.eq('seller_id', user.id);
|
||||
}
|
||||
|
||||
// Apply filters if provided
|
||||
if (filters) {
|
||||
// Token filter
|
||||
if (filters.token && filters.token !== 'all') {
|
||||
query = query.eq('token', filters.token);
|
||||
// For "my-ads", use Edge Function to bypass RLS (Telegram auth doesn't set auth.uid())
|
||||
if (type === 'my-ads') {
|
||||
const sessionToken = localStorage.getItem('p2p_session');
|
||||
if (!sessionToken) {
|
||||
setOffers([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fiat currency filter
|
||||
if (filters.fiatCurrency && filters.fiatCurrency !== 'all') {
|
||||
query = query.eq('fiat_currency', filters.fiatCurrency);
|
||||
const { data, error } = await supabase.functions.invoke('get-my-offers', {
|
||||
body: { sessionToken }
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Get my offers error:', error);
|
||||
setOffers([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Payment method filter
|
||||
if (filters.paymentMethods && filters.paymentMethods.length > 0) {
|
||||
query = query.in('payment_method_id', filters.paymentMethods);
|
||||
}
|
||||
|
||||
// Amount range filter
|
||||
if (filters.minAmount !== null) {
|
||||
query = query.gte('remaining_amount', filters.minAmount);
|
||||
}
|
||||
if (filters.maxAmount !== null) {
|
||||
query = query.lte('remaining_amount', filters.maxAmount);
|
||||
}
|
||||
|
||||
// Sort order
|
||||
const sortColumn = filters.sortBy === 'price' ? 'price_per_unit' :
|
||||
filters.sortBy === 'completion_rate' ? 'created_at' :
|
||||
filters.sortBy === 'trades' ? 'created_at' :
|
||||
'created_at';
|
||||
query = query.order(sortColumn, { ascending: filters.sortOrder === 'asc' });
|
||||
offersData = data?.offers || [];
|
||||
} else {
|
||||
query = query.order('created_at', { ascending: false });
|
||||
}
|
||||
// Build base query for public offers
|
||||
let query = supabase.from('p2p_fiat_offers').select('*');
|
||||
|
||||
const { data } = await query;
|
||||
offersData = data || [];
|
||||
if (type === 'buy') {
|
||||
// Buy tab = show SELL offers (user wants to buy from sellers)
|
||||
query = query.eq('ad_type', 'sell').eq('status', 'open').gt('remaining_amount', 0);
|
||||
} else if (type === 'sell') {
|
||||
// Sell tab = show BUY offers (user wants to sell to buyers)
|
||||
query = query.eq('ad_type', 'buy').eq('status', 'open').gt('remaining_amount', 0);
|
||||
}
|
||||
|
||||
// Apply filters if provided
|
||||
if (filters) {
|
||||
// Token filter
|
||||
if (filters.token && filters.token !== 'all') {
|
||||
query = query.eq('token', filters.token);
|
||||
}
|
||||
|
||||
// Fiat currency filter
|
||||
if (filters.fiatCurrency && filters.fiatCurrency !== 'all') {
|
||||
query = query.eq('fiat_currency', filters.fiatCurrency);
|
||||
}
|
||||
|
||||
// Payment method filter
|
||||
if (filters.paymentMethods && filters.paymentMethods.length > 0) {
|
||||
query = query.in('payment_method_id', filters.paymentMethods);
|
||||
}
|
||||
|
||||
// Amount range filter
|
||||
if (filters.minAmount !== null) {
|
||||
query = query.gte('remaining_amount', filters.minAmount);
|
||||
}
|
||||
if (filters.maxAmount !== null) {
|
||||
query = query.lte('remaining_amount', filters.maxAmount);
|
||||
}
|
||||
|
||||
// Sort order
|
||||
const sortColumn = filters.sortBy === 'price' ? 'price_per_unit' :
|
||||
filters.sortBy === 'completion_rate' ? 'created_at' :
|
||||
filters.sortBy === 'trades' ? 'created_at' :
|
||||
'created_at';
|
||||
query = query.order(sortColumn, { ascending: filters.sortOrder === 'asc' });
|
||||
} else {
|
||||
query = query.order('created_at', { ascending: false });
|
||||
}
|
||||
|
||||
const { data } = await query;
|
||||
offersData = data || [];
|
||||
}
|
||||
|
||||
// Enrich with reputation, payment method, and merchant tier
|
||||
const enrichedOffers = await Promise.all(
|
||||
|
||||
+34
-58
@@ -159,6 +159,7 @@ export interface CreateOfferParams {
|
||||
timeLimitMinutes?: number;
|
||||
minOrderAmount?: number;
|
||||
maxOrderAmount?: number;
|
||||
adType?: 'buy' | 'sell'; // Default: 'sell'
|
||||
}
|
||||
|
||||
export interface AcceptOfferParams {
|
||||
@@ -411,9 +412,9 @@ async function logAction(
|
||||
/**
|
||||
* Create a new P2P fiat offer (OKX-Style Internal Ledger)
|
||||
*
|
||||
* Steps:
|
||||
* Uses Edge Function to:
|
||||
* 1. Lock crypto from internal balance (NO blockchain tx)
|
||||
* 2. Create offer record in Supabase
|
||||
* 2. Create offer record in Supabase (bypasses RLS with service role)
|
||||
* 3. Update escrow tracking
|
||||
*/
|
||||
export async function createFiatOffer(params: CreateOfferParams): Promise<string> {
|
||||
@@ -426,74 +427,49 @@ export async function createFiatOffer(params: CreateOfferParams): Promise<string
|
||||
paymentDetails,
|
||||
timeLimitMinutes = DEFAULT_PAYMENT_DEADLINE_MINUTES,
|
||||
minOrderAmount,
|
||||
maxOrderAmount
|
||||
maxOrderAmount,
|
||||
adType = 'sell'
|
||||
} = params;
|
||||
|
||||
try {
|
||||
// Get current user
|
||||
const userId = await getCurrentUserId();
|
||||
if (!userId) throw new Error('Not authenticated');
|
||||
// Get session token for Edge Function authentication
|
||||
const sessionToken = localStorage.getItem('p2p_session');
|
||||
if (!sessionToken) throw new Error('Not authenticated. Please log in again.');
|
||||
|
||||
toast.info('Locking crypto from your balance...');
|
||||
toast.info('Creating offer...');
|
||||
|
||||
// 1. Lock crypto from internal balance (NO blockchain tx!)
|
||||
const { data: lockResult, error: lockError } = await supabase.rpc('lock_escrow_internal', {
|
||||
p_user_id: userId,
|
||||
p_token: token,
|
||||
p_amount: amountCrypto
|
||||
});
|
||||
|
||||
if (lockError) throw lockError;
|
||||
|
||||
// Parse result
|
||||
const lockResponse = typeof lockResult === 'string' ? JSON.parse(lockResult) : lockResult;
|
||||
|
||||
if (!lockResponse.success) {
|
||||
throw new Error(lockResponse.error || 'Failed to lock balance');
|
||||
}
|
||||
|
||||
toast.success('Balance locked successfully');
|
||||
|
||||
// 2. Encrypt payment details (AES-256-GCM)
|
||||
// Encrypt payment details (AES-256-GCM) before sending
|
||||
const encryptedDetails = await encryptPaymentDetails(paymentDetails);
|
||||
|
||||
// 3. Create offer in Supabase
|
||||
const { data: offer, error: offerError } = await supabase
|
||||
.from('p2p_fiat_offers')
|
||||
.insert({
|
||||
seller_id: userId,
|
||||
seller_wallet: '', // No longer needed with internal ledger
|
||||
// Call Edge Function (handles escrow locking + offer creation with service role)
|
||||
const { data, error } = await supabase.functions.invoke('create-offer-telegram', {
|
||||
body: {
|
||||
sessionToken,
|
||||
token,
|
||||
amount_crypto: amountCrypto,
|
||||
fiat_currency: fiatCurrency,
|
||||
fiat_amount: fiatAmount,
|
||||
price_per_unit: fiatAmount / amountCrypto,
|
||||
payment_method_id: paymentMethodId,
|
||||
payment_details_encrypted: encryptedDetails,
|
||||
min_order_amount: minOrderAmount,
|
||||
max_order_amount: maxOrderAmount,
|
||||
time_limit_minutes: timeLimitMinutes,
|
||||
status: 'open',
|
||||
remaining_amount: amountCrypto,
|
||||
escrow_locked_at: new Date().toISOString()
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (offerError) throw offerError;
|
||||
|
||||
// 4. Audit log
|
||||
await logAction('offer', offer.id, 'create_offer', {
|
||||
token,
|
||||
amount_crypto: amountCrypto,
|
||||
fiat_currency: fiatCurrency,
|
||||
fiat_amount: fiatAmount,
|
||||
escrow_type: 'internal_ledger'
|
||||
amountCrypto,
|
||||
fiatCurrency,
|
||||
fiatAmount,
|
||||
paymentMethodId,
|
||||
paymentDetailsEncrypted: encryptedDetails,
|
||||
minOrderAmount: minOrderAmount || null,
|
||||
maxOrderAmount: maxOrderAmount || null,
|
||||
timeLimitMinutes,
|
||||
adType
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Edge Function error:', error);
|
||||
throw new Error(error.message || 'Failed to create offer');
|
||||
}
|
||||
|
||||
if (!data?.success) {
|
||||
throw new Error(data?.error || 'Failed to create offer');
|
||||
}
|
||||
|
||||
toast.success(`Offer created! Selling ${amountCrypto} ${token} for ${fiatAmount} ${fiatCurrency}`);
|
||||
|
||||
return offer.id;
|
||||
return data.offer_id;
|
||||
} catch (error: unknown) {
|
||||
console.error('Create offer error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Failed to create offer';
|
||||
|
||||
Reference in New Issue
Block a user