From 698c014682eee37a5e46acbe3fea9f2e82008414 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Mon, 27 Apr 2026 13:30:09 +0300 Subject: [PATCH] feat: unified P2P identity across Telegram mini app and pwap/web MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add p2p_user_id column to tg_users to bridge citizen/visa UUID (v5) used by pwap/web with the Supabase Auth UUID (v4) used by the mini app. - Migration: tg_users.p2p_user_id UUID (nullable, indexed) - 6 P2P edge functions: replace listUsers+find with direct tg_users lookup — resolves userId as p2p_user_id ?? id (backwards compatible) - Eliminates O(N) auth.admin.listUsers scan in every P2P call When p2p_user_id is populated (via TelegramConnect wallet link), mini app users share the same P2P balance and offers as pwap/web. --- package.json | 2 +- src/version.json | 6 +- supabase/functions/accept-p2p-offer/index.ts | 39 +++---- .../functions/create-offer-telegram/index.ts | 17 +-- supabase/functions/p2p-dispute/index.ts | 102 ++++++++++++----- supabase/functions/p2p-messages/index.ts | 22 ++-- .../request-withdraw-telegram/index.ts | 106 ++++++++++-------- supabase/functions/trade-action/index.ts | 94 ++++++++++------ .../migrations/20260427_add_p2p_user_id.sql | 9 ++ 9 files changed, 245 insertions(+), 152 deletions(-) create mode 100644 supabase/migrations/20260427_add_p2p_user_id.sql diff --git a/package.json b/package.json index 6b8cee7..6852195 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.229", + "version": "1.0.230", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/version.json b/src/version.json index bbd78d3..c9cd1a9 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.229", - "buildTime": "2026-02-27T23:33:39.279Z", - "buildNumber": 1772235219280 + "version": "1.0.230", + "buildTime": "2026-04-27T10:30:10.174Z", + "buildNumber": 1777285810176 } diff --git a/supabase/functions/accept-p2p-offer/index.ts b/supabase/functions/accept-p2p-offer/index.ts index 83a02df..93716b4 100644 --- a/supabase/functions/accept-p2p-offer/index.ts +++ b/supabase/functions/accept-p2p-offer/index.ts @@ -130,10 +130,13 @@ serve(async (req) => { // Validate required fields if (!offerId || !amount || !buyerWallet) { - return new Response(JSON.stringify({ error: 'Missing required fields: offerId, amount, buyerWallet' }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ error: 'Missing required fields: offerId, amount, buyerWallet' }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } if (amount <= 0) { @@ -148,21 +151,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user ID for this telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: authUsers }, - } = await supabase.auth.admin.listUsers({ perPage: 1000 }); - const authUser = authUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await supabase + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response(JSON.stringify({ error: 'User not found. Please authenticate first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // Call the accept_p2p_offer RPC function const { data: rpcResult, error: rpcError } = await supabase.rpc('accept_p2p_offer', { @@ -187,13 +191,10 @@ serve(async (req) => { const result = typeof rpcResult === 'string' ? JSON.parse(rpcResult) : rpcResult; if (!result.success) { - return new Response( - JSON.stringify({ error: result.error || 'Failed to accept offer' }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - } - ); + return new Response(JSON.stringify({ error: result.error || 'Failed to accept offer' }), { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); } // Log to p2p_audit_log diff --git a/supabase/functions/create-offer-telegram/index.ts b/supabase/functions/create-offer-telegram/index.ts index 7b1b311..73ac42d 100644 --- a/supabase/functions/create-offer-telegram/index.ts +++ b/supabase/functions/create-offer-telegram/index.ts @@ -160,21 +160,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user ID for this telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: authUsers }, - } = await supabase.auth.admin.listUsers({ perPage: 1000 }); - const authUser = authUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await supabase + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response(JSON.stringify({ error: 'User not found. Please authenticate first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // 1. Lock escrow from internal balance const { data: lockResult, error: lockError } = await supabase.rpc('lock_escrow_internal', { diff --git a/supabase/functions/p2p-dispute/index.ts b/supabase/functions/p2p-dispute/index.ts index d8b6194..8a4f925 100644 --- a/supabase/functions/p2p-dispute/index.ts +++ b/supabase/functions/p2p-dispute/index.ts @@ -28,7 +28,13 @@ interface P2PDisputeRequest { reason?: string; category?: 'payment_not_received' | 'wrong_amount' | 'fake_payment_proof' | 'other'; evidenceUrl?: string; - evidenceType?: 'screenshot' | 'receipt' | 'bank_statement' | 'chat_log' | 'transaction_proof' | 'other'; + evidenceType?: + | 'screenshot' + | 'receipt' + | 'bank_statement' + | 'chat_log' + | 'transaction_proof' + | 'other'; description?: string; } @@ -97,7 +103,16 @@ serve(async (req) => { try { const body: P2PDisputeRequest = await req.json(); - const { sessionToken, action, tradeId, reason, category, evidenceUrl, evidenceType, description } = body; + const { + sessionToken, + action, + tradeId, + reason, + category, + evidenceUrl, + evidenceType, + description, + } = body; // Get bot tokens for session verification (dual bot support) const botTokens: string[] = []; @@ -141,10 +156,13 @@ serve(async (req) => { } if (!['open', 'add_evidence'].includes(action)) { - return new Response(JSON.stringify({ error: 'Invalid action. Must be "open" or "add_evidence"' }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ error: 'Invalid action. Must be "open" or "add_evidence"' }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Create Supabase admin client (bypasses RLS) @@ -152,21 +170,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user ID for this telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: authUsers }, - } = await supabase.auth.admin.listUsers({ perPage: 1000 }); - const authUser = authUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await supabase + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response(JSON.stringify({ error: 'User not found. Please authenticate first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // Verify user is a party to this trade const { data: trade, error: tradeError } = await supabase @@ -193,20 +212,35 @@ serve(async (req) => { if (action === 'open') { // Trade must be in active status to dispute if (!['pending', 'payment_sent'].includes(trade.status)) { - return new Response(JSON.stringify({ error: `Cannot open dispute: trade status is '${trade.status}', must be 'pending' or 'payment_sent'` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: `Cannot open dispute: trade status is '${trade.status}', must be 'pending' or 'payment_sent'`, + }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Validate category - const validCategories = ['payment_not_received', 'wrong_amount', 'fake_payment_proof', 'other']; + const validCategories = [ + 'payment_not_received', + 'wrong_amount', + 'fake_payment_proof', + 'other', + ]; const disputeCategory = category || 'other'; if (!validCategories.includes(disputeCategory)) { - return new Response(JSON.stringify({ error: `Invalid category. Must be one of: ${validCategories.join(', ')}` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: `Invalid category. Must be one of: ${validCategories.join(', ')}`, + }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } const disputeReason = reason || 'No reason provided'; @@ -300,13 +334,25 @@ serve(async (req) => { }); } - const validEvidenceTypes = ['screenshot', 'receipt', 'bank_statement', 'chat_log', 'transaction_proof', 'other']; + const validEvidenceTypes = [ + 'screenshot', + 'receipt', + 'bank_statement', + 'chat_log', + 'transaction_proof', + 'other', + ]; const evType = evidenceType || 'other'; if (!validEvidenceTypes.includes(evType)) { - return new Response(JSON.stringify({ error: `Invalid evidence type. Must be one of: ${validEvidenceTypes.join(', ')}` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: `Invalid evidence type. Must be one of: ${validEvidenceTypes.join(', ')}`, + }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Find the dispute for this trade diff --git a/supabase/functions/p2p-messages/index.ts b/supabase/functions/p2p-messages/index.ts index 3437b3c..c9d4745 100644 --- a/supabase/functions/p2p-messages/index.ts +++ b/supabase/functions/p2p-messages/index.ts @@ -148,21 +148,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user ID for this telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: authUsers }, - } = await supabase.auth.admin.listUsers({ perPage: 1000 }); - const authUser = authUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await supabase + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response(JSON.stringify({ error: 'User not found. Please authenticate first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // Verify user is a party to this trade const { data: trade, error: tradeError } = await supabase @@ -248,7 +249,10 @@ serve(async (req) => { // Mark unread messages as read for this user // (messages sent by the OTHER party that haven't been read yet) const unreadMessageIds = (messages || []) - .filter((m: { sender_id: string; is_read: boolean; id: string }) => m.sender_id !== userId && !m.is_read) + .filter( + (m: { sender_id: string; is_read: boolean; id: string }) => + m.sender_id !== userId && !m.is_read + ) .map((m: { id: string }) => m.id); if (unreadMessageIds.length > 0) { diff --git a/supabase/functions/request-withdraw-telegram/index.ts b/supabase/functions/request-withdraw-telegram/index.ts index ff17c3f..59c591e 100644 --- a/supabase/functions/request-withdraw-telegram/index.ts +++ b/supabase/functions/request-withdraw-telegram/index.ts @@ -162,44 +162,55 @@ async function sendTokens( return new Promise((resolve) => { let txHash: string; - tx.signAndSend(hotWallet, { nonce }, (result: { txHash: { toHex: () => string }; status: { isInBlock: boolean; asInBlock: { toHex: () => string }; isFinalized: boolean }; dispatchError: { isModule: boolean; asModule: unknown; toString: () => string } | undefined; isError: boolean }) => { - txHash = result.txHash.toHex(); + tx.signAndSend( + hotWallet, + { nonce }, + (result: { + txHash: { toHex: () => string }; + status: { isInBlock: boolean; asInBlock: { toHex: () => string }; isFinalized: boolean }; + dispatchError: + | { isModule: boolean; asModule: unknown; toString: () => string } + | undefined; + isError: boolean; + }) => { + txHash = result.txHash.toHex(); - if (result.status.isInBlock) { - console.log(`TX in block: ${result.status.asInBlock.toHex()}`); - } + if (result.status.isInBlock) { + console.log(`TX in block: ${result.status.asInBlock.toHex()}`); + } - if (result.status.isFinalized) { - const dispatchError = result.dispatchError; + if (result.status.isFinalized) { + const dispatchError = result.dispatchError; - if (dispatchError) { - if (dispatchError.isModule) { - const decoded = api.registry.findMetaError(dispatchError.asModule); - resolve({ - success: false, - txHash, - error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`, - }); + if (dispatchError) { + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + resolve({ + success: false, + txHash, + error: `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`, + }); + } else { + resolve({ + success: false, + txHash, + error: dispatchError.toString(), + }); + } } else { - resolve({ - success: false, - txHash, - error: dispatchError.toString(), - }); + resolve({ success: true, txHash }); } - } else { - resolve({ success: true, txHash }); + } + + if (result.isError) { + resolve({ + success: false, + txHash, + error: 'Transaction failed', + }); } } - - if (result.isError) { - resolve({ - success: false, - txHash, - error: 'Transaction failed', - }); - } - }).catch((error: Error) => { + ).catch((error: Error) => { resolve({ success: false, error: error.message }); }); @@ -250,18 +261,18 @@ serve(async (req) => { if (_mainToken) botTokens.push(_mainToken); if (_krdToken) botTokens.push(_krdToken); if (botTokens.length === 0) { - return new Response( - JSON.stringify({ success: false, error: 'Server configuration error' }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ); + return new Response(JSON.stringify({ success: false, error: 'Server configuration error' }), { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); } // Validate session token if (!sessionToken) { - return new Response( - JSON.stringify({ success: false, error: 'Missing session token' }), - { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } - ); + return new Response(JSON.stringify({ success: false, error: 'Missing session token' }), { + status: 401, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); } let telegramId: number | null = null; @@ -281,21 +292,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const serviceClient = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user for telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: existingUsers }, - } = await serviceClient.auth.admin.listUsers({ perPage: 1000 }); - const authUser = existingUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await serviceClient + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response( JSON.stringify({ success: false, error: 'User not found. Please deposit first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // Validate input if (!token || !amount || !walletAddress) { diff --git a/supabase/functions/trade-action/index.ts b/supabase/functions/trade-action/index.ts index 863e2f4..46b0f3d 100644 --- a/supabase/functions/trade-action/index.ts +++ b/supabase/functions/trade-action/index.ts @@ -142,10 +142,13 @@ serve(async (req) => { const validActions = ['mark_paid', 'confirm', 'cancel', 'rate']; if (!validActions.includes(action)) { - return new Response(JSON.stringify({ error: `Invalid action. Must be one of: ${validActions.join(', ')}` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ error: `Invalid action. Must be one of: ${validActions.join(', ')}` }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Create Supabase admin client (bypasses RLS) @@ -153,21 +156,22 @@ serve(async (req) => { const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth user ID for this telegram user - const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`; - const { - data: { users: authUsers }, - } = await supabase.auth.admin.listUsers({ perPage: 1000 }); - const authUser = authUsers?.find((u: { email?: string }) => u.email === telegramEmail); + // Resolve P2P user ID: prefer p2p_user_id (citizen/visa UUID, same as pwap/web) + // fallback to tg_users.id (legacy Supabase Auth UUID) + const { data: tgUser, error: tgUserError } = await supabase + .from('tg_users') + .select('id, p2p_user_id') + .eq('telegram_id', telegramId) + .single(); - if (!authUser) { + if (tgUserError || !tgUser) { return new Response(JSON.stringify({ error: 'User not found. Please authenticate first.' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } - const userId = authUser.id; + const userId = tgUser.p2p_user_id ?? tgUser.id; // Fetch the trade const { data: trade, error: tradeError } = await supabase @@ -205,10 +209,15 @@ serve(async (req) => { // Trade must be in pending status if (trade.status !== 'pending') { - return new Response(JSON.stringify({ error: `Cannot mark paid: trade status is '${trade.status}', expected 'pending'` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: `Cannot mark paid: trade status is '${trade.status}', expected 'pending'`, + }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } const now = new Date().toISOString(); @@ -256,18 +265,26 @@ serve(async (req) => { else if (action === 'confirm') { // Only seller can confirm if (trade.seller_id !== userId) { - return new Response(JSON.stringify({ error: 'Only the seller can confirm and release crypto' }), { - status: 403, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ error: 'Only the seller can confirm and release crypto' }), + { + status: 403, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Trade must be in payment_sent status if (trade.status !== 'payment_sent') { - return new Response(JSON.stringify({ error: `Cannot confirm: trade status is '${trade.status}', expected 'payment_sent'` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ + error: `Cannot confirm: trade status is '${trade.status}', expected 'payment_sent'`, + }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } // Get offer details to know the token @@ -308,7 +325,8 @@ serve(async (req) => { ); } - const releaseResponse = typeof releaseResult === 'string' ? JSON.parse(releaseResult) : releaseResult; + const releaseResponse = + typeof releaseResult === 'string' ? JSON.parse(releaseResult) : releaseResult; if (releaseResponse && !releaseResponse.success) { return new Response( @@ -337,7 +355,9 @@ serve(async (req) => { if (updateError) { console.error('Confirm trade update error:', updateError); return new Response( - JSON.stringify({ error: 'Escrow released but failed to update trade status: ' + updateError.message }), + JSON.stringify({ + error: 'Escrow released but failed to update trade status: ' + updateError.message, + }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, @@ -365,10 +385,13 @@ serve(async (req) => { else if (action === 'cancel') { // Trade must be in pending or payment_sent status to cancel if (!['pending', 'payment_sent'].includes(trade.status)) { - return new Response(JSON.stringify({ error: `Cannot cancel: trade status is '${trade.status}'` }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); + return new Response( + JSON.stringify({ error: `Cannot cancel: trade status is '${trade.status}'` }), + { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + } + ); } const cancelReason = payload?.reason || 'Cancelled by user'; @@ -441,13 +464,10 @@ serve(async (req) => { const result = typeof rpcResult === 'string' ? JSON.parse(rpcResult) : rpcResult; if (result && !result.success) { - return new Response( - JSON.stringify({ error: result.error || 'Failed to cancel trade' }), - { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - } - ); + return new Response(JSON.stringify({ error: result.error || 'Failed to cancel trade' }), { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); } // Fetch updated trade diff --git a/supabase/migrations/20260427_add_p2p_user_id.sql b/supabase/migrations/20260427_add_p2p_user_id.sql new file mode 100644 index 0000000..59a3144 --- /dev/null +++ b/supabase/migrations/20260427_add_p2p_user_id.sql @@ -0,0 +1,9 @@ +-- Add p2p_user_id to tg_users for unified P2P identity across platforms +-- +-- p2p_user_id = UUID v5 derived from citizen/visa number (same as pwap/web) +-- When a Telegram user links their wallet, this is populated. +-- Edge functions use: p2p_user_id ?? id (fallback keeps old behaviour) + +ALTER TABLE tg_users ADD COLUMN IF NOT EXISTS p2p_user_id UUID; + +CREATE INDEX IF NOT EXISTS idx_tg_users_p2p_user_id ON tg_users(p2p_user_id);