mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-06-12 10:11:01 +00:00
feat: unified P2P identity across Telegram mini app and pwap/web
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.
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user