feat: add Telegram mini app connect for P2P access

This commit is contained in:
2026-01-29 21:27:13 +03:00
parent 68a5b96bbd
commit 72d3b9204a
48 changed files with 2558 additions and 9502 deletions
@@ -9,7 +9,7 @@
//!
//! > If starting a new teyrchain project, please use an async backing compatible template such as
//! > the
//! > [teyrchain template](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/templates/teyrchain).
//! > [teyrchain template](https://github.com/pezkuwichain/pezkuwi-sdk/tree/main/templates/teyrchain).
//! The rollout process for Async Backing has three phases. Phases 1 and 2 below put new
//! infrastructure in place. Then we can simply turn on async backing in phase 3.
//!
@@ -330,6 +330,7 @@
//! [`pezpallet::pezpallet`]: pezframe_support::pezpallet
//! [`pezpallet::config`]: pezframe_support::pezpallet_macros::config
//! [`pezpallet::generate_deposit`]: pezframe_support::pezpallet_macros::generate_deposit
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
#[docify::export]
#[pezframe::pezpallet(dev_mode)]
@@ -169,6 +169,7 @@
//! [`crate::pezkuwi_sdk::templates`].
//!
//! [`SolochainDefaultConfig`]: struct@pezframe_system::pezpallet::config_preludes::SolochainDefaultConfig
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
#[cfg(test)]
mod tests {
@@ -133,6 +133,8 @@
//! - [`pezsc_consensus_beefy`] (TODO: @adrian, add some high level docs <https://github.com/pezkuwichain/pezkuwi-sdk/issues/305>)
//! - [`pezsc_consensus_manual_seal`]
//! - [`pezsc_consensus_pow`]
//!
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
#[doc(hidden)]
pub use crate::pezkuwi_sdk;
@@ -111,4 +111,6 @@
//!
//! - <https://forum.polkadot.network/t/offchain-workers-design-assumptions-vulnerabilities/2548>
//! - <https://exchange.pezkuwichain.app/questions/11058/how-can-i-create-ocw-that-wont-activates-every-block-but-will-activates-only-w/11060#11060>
//! - [Offchain worker example](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/pezframe/examples/offchain-worker)
//! - [Offchain worker example](https://github.com/pezkuwichain/pezkuwi-sdk/tree/main/bizinikiwi/pezframe/examples/offchain-worker)
//!
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
@@ -54,7 +54,7 @@
//! #### Dispatchable:
//!
//! Dispatchables are [function objects](https://en.wikipedia.org/wiki/Function_object) that act as
//! the entry points in [FRAME](frame) pallets. They can be called by internal or external entities
//! the entry points in [FRAME](crate::pezkuwi_sdk::frame_runtime) pallets. They can be called by internal or external entities
//! to interact with the blockchain's state. They are a core aspect of the runtime logic, handling
//! transactions and other state-changing operations.
//!
@@ -68,7 +68,7 @@
//!
//! #### Pezpallet
//!
//! Similar to software modules in traditional programming, [FRAME](frame) pallets in Bizinikiwi are
//! Similar to software modules in traditional programming, [FRAME](crate::pezkuwi_sdk::frame_runtime) pallets in Bizinikiwi are
//! modular components that encapsulate distinct functionalities or business logic. Just as
//! libraries or modules are used to build and extend the capabilities of a software application,
//! pallets are the foundational building blocks for constructing a blockchain's runtime with frame.
@@ -118,3 +118,4 @@
//! network.
//!
//! **Synonyms**: Teyrchain Validation Function
//!
@@ -93,10 +93,10 @@ pub mod cli;
pub mod frame_runtime_upgrades_and_migrations;
/// Learn about the offchain workers, how they function, and how to use them, as provided by the
/// [`frame`] APIs.
/// [`crate::pezkuwi_sdk::frame_runtime`] APIs.
pub mod frame_offchain_workers;
/// Learn about the different ways through which multiple [`frame`] pallets can be combined to work
/// Learn about the different ways through which multiple [`crate::pezkuwi_sdk::frame_runtime`] pallets can be combined to work
/// together.
pub mod frame_pallet_coupling;
@@ -19,7 +19,7 @@
//!
//! #### Smart Contracts in Bizinikiwi
//! Smart Contracts are autonomous, programmable constructs deployed on the blockchain.
//! In [FRAME](frame), Smart Contracts infrastructure is implemented by the
//! In [FRAME](crate::pezkuwi_sdk::frame_runtime), Smart Contracts infrastructure is implemented by the
//! [`pezpallet_contracts`] for WASM-based contracts or the
//! [`pezpallet_evm`](https://github.com/polkadot-evm/frontier/tree/master/frame/evm) for EVM-compatible contracts. These pallets
//! enable Smart Contract developers to build applications and systems on top of a Bizinikiwi-based
@@ -207,3 +207,4 @@
//! - **For Smart Contract Developers**: Being mindful of the gas cost associated with contract
//! execution is crucial. Efficiently written contracts save costs and are less likely to hit gas
//! limits, ensuring smoother execution on the blockchain.
//!
@@ -111,6 +111,8 @@
//! - <https://github.com/pezkuwichain/pezkuwi-sdk/issues/326>
//! - [Bizinikiwi Seminar - Traits and Generic Types](https://www.youtube.com/watch?v=6cp10jVWNl4)
//! - <https://exchange.pezkuwichain.app/questions/2228/type-casting-to-trait-t-as-config>
//!
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
#![allow(unused)]
use pezframe::traits::Get;
+7 -5
View File
@@ -22,6 +22,7 @@ initSentry();
// Lazy load pages for code splitting
const Index = lazy(() => import('@/pages/Index'));
const Login = lazy(() => import('@/pages/Login'));
const TelegramConnect = lazy(() => import('@/pages/TelegramConnect'));
const Dashboard = lazy(() => import('@/pages/Dashboard'));
const EmailVerification = lazy(() => import('@/pages/EmailVerification'));
const PasswordReset = lazy(() => import('@/pages/PasswordReset'));
@@ -113,6 +114,7 @@ function App() {
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/auth/telegram-connect" element={<TelegramConnect />} />
<Route path="/email-verification" element={<EmailVerification />} />
<Route path="/reset-password" element={<PasswordReset />} />
<Route path="/" element={<Index />} />
@@ -177,27 +179,27 @@ function App() {
</ProtectedRoute>
} />
<Route path="/p2p" element={
<ProtectedRoute>
<ProtectedRoute allowTelegramSession>
<P2PPlatform />
</ProtectedRoute>
} />
<Route path="/p2p/trade/:tradeId" element={
<ProtectedRoute>
<ProtectedRoute allowTelegramSession>
<P2PTrade />
</ProtectedRoute>
} />
<Route path="/p2p/orders" element={
<ProtectedRoute>
<ProtectedRoute allowTelegramSession>
<P2POrders />
</ProtectedRoute>
} />
<Route path="/p2p/dispute/:disputeId" element={
<ProtectedRoute>
<ProtectedRoute allowTelegramSession>
<P2PDispute />
</ProtectedRoute>
} />
<Route path="/p2p/merchant" element={
<ProtectedRoute>
<ProtectedRoute allowTelegramSession>
<P2PMerchantDashboard />
</ProtectedRoute>
} />
+24 -2
View File
@@ -8,16 +8,37 @@ import { Button } from '@/components/ui/button';
interface ProtectedRouteProps {
children: React.ReactNode;
requireAdmin?: boolean;
allowTelegramSession?: boolean;
}
// Check if valid telegram session exists
function getTelegramSession(): { telegram_id: string; wallet_address: string; username: string } | null {
try {
const session = localStorage.getItem('telegram_session');
if (!session) return null;
const parsed = JSON.parse(session);
// Session expires after 24 hours
if (Date.now() - parsed.timestamp > 24 * 60 * 60 * 1000) {
localStorage.removeItem('telegram_session');
return null;
}
return parsed;
} catch {
return null;
}
}
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
children,
requireAdmin = false
requireAdmin = false,
allowTelegramSession = false
}) => {
const { user, loading, isAdmin } = useAuth();
const { selectedAccount, connectWallet } = usePezkuwi();
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0);
const telegramSession = allowTelegramSession ? getTelegramSession() : null;
// Listen for wallet changes
useEffect(() => {
@@ -84,7 +105,8 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
);
}
if (!user) {
// Allow access if user is logged in OR has valid telegram session
if (!user && !telegramSession) {
return <Navigate to="/login" replace />;
}
+183
View File
@@ -0,0 +1,183 @@
/**
* Telegram Mini App Connect Page
* Handles authentication from Telegram mini app and redirects to P2P
*/
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react';
type Status = 'loading' | 'connecting' | 'success' | 'error';
export default function TelegramConnect() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [status, setStatus] = useState<Status>('loading');
const [message, setMessage] = useState('Girêdan tê kirin...');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const connect = async () => {
try {
// Get params from URL
const telegramId = searchParams.get('tg_id');
const walletAddress = searchParams.get('wallet');
const timestamp = searchParams.get('ts');
const from = searchParams.get('from');
// Validate params
if (!telegramId || from !== 'miniapp') {
setStatus('error');
setError('Parametreyên nederbasdar. Ji kerema xwe ji mini app-ê dest pê bikin.');
return;
}
// Check timestamp (allow 5 minutes)
if (timestamp) {
const ts = parseInt(timestamp, 10);
const now = Date.now();
if (now - ts > 5 * 60 * 1000) {
setStatus('error');
setError('Lînk qediya. Ji kerema xwe dîsa biceribînin.');
return;
}
}
setStatus('connecting');
setMessage('Bikarhêner tê pejirandin...');
// Find user by telegram_id
const { data: userData, error: userError } = await supabase
.from('users')
.select('id, telegram_id, wallet_address, username, first_name')
.eq('telegram_id', parseInt(telegramId, 10))
.single();
if (userError || !userData) {
setStatus('error');
setError('Bikarhêner nehat dîtin. Ji kerema xwe berî dest bi P2P-ê bikin, di mini app-ê de cîzdanê xwe ava bikin.');
return;
}
// Update wallet address if provided and different
if (walletAddress && walletAddress !== userData.wallet_address) {
await supabase
.from('users')
.update({ wallet_address: walletAddress })
.eq('id', userData.id);
}
// Generate email for this telegram user (for Supabase auth)
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
// Try to sign in with magic link (will be sent to email, but we'll catch it)
// Or check if user already has an auth account
const { data: authData } = await supabase.auth.getSession();
if (authData?.session) {
// Already logged in, redirect to P2P
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setTimeout(() => navigate('/p2p'), 1000);
return;
}
// Try to sign in with OTP/magic link
const { error: signInError } = await supabase.auth.signInWithOtp({
email: telegramEmail,
options: {
shouldCreateUser: true,
data: {
telegram_id: parseInt(telegramId, 10),
wallet_address: walletAddress,
username: userData.username || userData.first_name,
},
},
});
if (signInError) {
// If OTP fails, try password-less sign in
console.error('OTP sign in failed:', signInError);
// Store telegram session info in localStorage for P2P access
localStorage.setItem('telegram_session', JSON.stringify({
telegram_id: telegramId,
wallet_address: walletAddress,
username: userData.username || userData.first_name,
timestamp: Date.now(),
}));
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setTimeout(() => navigate('/p2p'), 1000);
return;
}
// Success - redirect to P2P
setStatus('success');
setMessage('Serketî! Tê veguheztin...');
setTimeout(() => navigate('/p2p'), 1000);
} catch (err) {
console.error('Telegram connect error:', err);
setStatus('error');
setError('Xeletî di girêdanê de. Ji kerema xwe dîsa biceribînin.');
}
};
connect();
}, [searchParams, navigate]);
return (
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-gray-900 rounded-2xl p-8 text-center">
{/* Logo */}
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-gradient-to-br from-green-500 to-yellow-500 flex items-center justify-center">
{status === 'loading' || status === 'connecting' ? (
<Loader2 className="w-8 h-8 text-white animate-spin" />
) : status === 'success' ? (
<CheckCircle2 className="w-8 h-8 text-white" />
) : (
<AlertTriangle className="w-8 h-8 text-white" />
)}
</div>
{/* Title */}
<h1 className="text-xl font-semibold text-white mb-2">
{status === 'error' ? 'Xeletî' : 'Telegram Connect'}
</h1>
{/* Status Message */}
<p className={`text-sm ${status === 'error' ? 'text-red-400' : 'text-gray-400'}`}>
{error || message}
</p>
{/* Error Action */}
{status === 'error' && (
<div className="mt-6 space-y-3">
<button
onClick={() => window.close()}
className="w-full py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-xl font-medium transition-colors"
>
Pencereyê Bigire
</button>
<p className="text-xs text-gray-500">
Ji kerema xwe vegerin mini app-ê û dîsa biceribînin
</p>
</div>
)}
{/* Success Info */}
{status === 'success' && (
<div className="mt-6">
<div className="flex items-center justify-center gap-2 text-green-400">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
<span className="text-sm">P2P Platform vekirin...</span>
</div>
</div>
)}
</div>
</div>
);
}
+313
View File
@@ -0,0 +1,313 @@
-- =====================================================
-- P2P END-TO-END TEST SCENARIO
-- Alice sells 200 HEZ, Bob buys 150 HEZ with IQD
-- Uses REAL users from auth.users table
-- =====================================================
BEGIN;
DO $$
DECLARE
v_alice_id UUID;
v_bob_id UUID;
v_alice_wallet TEXT := '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
v_bob_wallet TEXT := '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
v_payment_method_id UUID;
v_offer_id UUID;
v_trade_id UUID;
v_result JSON;
v_alice_available DECIMAL;
v_alice_locked DECIMAL;
v_bob_available DECIMAL;
v_offer_remaining DECIMAL;
v_trade_status TEXT;
v_user_count INT;
BEGIN
RAISE NOTICE '';
RAISE NOTICE '================================================';
RAISE NOTICE 'P2P E2E TEST: Alice sells 200 HEZ, Bob buys 150';
RAISE NOTICE '================================================';
-- =====================================================
-- STEP 0: Get real users from auth.users
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 0: Finding test users ---';
-- Get first two users from auth.users
SELECT COUNT(*) INTO v_user_count FROM auth.users;
IF v_user_count < 2 THEN
RAISE EXCEPTION 'Need at least 2 users in auth.users table. Found: %', v_user_count;
END IF;
-- Alice = first user, Bob = second user
SELECT id INTO v_alice_id FROM auth.users ORDER BY created_at LIMIT 1;
SELECT id INTO v_bob_id FROM auth.users ORDER BY created_at LIMIT 1 OFFSET 1;
RAISE NOTICE ' Found % users in auth.users', v_user_count;
RAISE NOTICE ' Alice (User 1): %', v_alice_id;
RAISE NOTICE ' Bob (User 2): %', v_bob_id;
-- =====================================================
-- CLEANUP: Remove any existing test data for these users
-- =====================================================
DELETE FROM p2p_balance_transactions WHERE user_id IN (v_alice_id, v_bob_id);
DELETE FROM p2p_fiat_trades WHERE seller_id = v_alice_id OR buyer_id IN (v_alice_id, v_bob_id);
DELETE FROM p2p_fiat_offers WHERE seller_id = v_alice_id;
DELETE FROM user_internal_balances WHERE user_id IN (v_alice_id, v_bob_id);
RAISE NOTICE ' Cleaned up previous test data';
-- =====================================================
-- STEP 1: Alice deposits 200 HEZ
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 1: Alice deposits 200 HEZ ---';
INSERT INTO user_internal_balances (user_id, token, available_balance, total_deposited, last_deposit_at)
VALUES (v_alice_id, 'HEZ', 200, 200, NOW());
INSERT INTO p2p_balance_transactions (user_id, token, transaction_type, amount, balance_before, balance_after, description)
VALUES (v_alice_id, 'HEZ', 'deposit', 200, 0, 200, 'Test deposit');
-- Bob's empty balance
INSERT INTO user_internal_balances (user_id, token, available_balance, total_deposited)
VALUES (v_bob_id, 'HEZ', 0, 0);
RAISE NOTICE ' ✓ Alice deposited 200 HEZ';
-- =====================================================
-- STEP 2: Get payment method
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 2: Get ZainCash payment method ---';
SELECT id INTO v_payment_method_id
FROM payment_methods
WHERE currency = 'IQD' AND method_name = 'ZainCash'
LIMIT 1;
IF v_payment_method_id IS NULL THEN
INSERT INTO payment_methods (currency, country, method_name, method_type, fields)
VALUES ('IQD', 'Iraq', 'ZainCash', 'mobile_payment', '{"phone_number": "ZainCash Phone"}')
RETURNING id INTO v_payment_method_id;
RAISE NOTICE ' ✓ Created ZainCash payment method';
ELSE
RAISE NOTICE ' ✓ Using existing ZainCash: %', v_payment_method_id;
END IF;
-- =====================================================
-- STEP 3: Alice creates sell offer for 200 HEZ
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 3: Alice creates sell offer (200 HEZ) ---';
-- Lock escrow
SELECT lock_escrow_internal(v_alice_id, 'HEZ', 200, 'offer', NULL) INTO v_result;
IF NOT (v_result->>'success')::boolean THEN
RAISE EXCEPTION 'Escrow lock failed: %', v_result->>'error';
END IF;
RAISE NOTICE ' ✓ Escrow locked: 200 HEZ';
-- Create offer
INSERT INTO p2p_fiat_offers (
seller_id, seller_wallet, token, amount_crypto, fiat_currency, fiat_amount,
payment_method_id, payment_details_encrypted, min_order_amount, max_order_amount,
time_limit_minutes, status, remaining_amount, ad_type
) VALUES (
v_alice_id, v_alice_wallet, 'HEZ', 200, 'IQD', 30000000,
v_payment_method_id, 'encrypted_+9647701234567', 10, 200,
30, 'open', 200, 'sell'
) RETURNING id INTO v_offer_id;
RAISE NOTICE ' ✓ Offer created: %', v_offer_id;
RAISE NOTICE ' 200 HEZ for 30,000,000 IQD (150,000 IQD/HEZ)';
-- Check Alice balance
SELECT available_balance, locked_balance INTO v_alice_available, v_alice_locked
FROM user_internal_balances WHERE user_id = v_alice_id AND token = 'HEZ';
RAISE NOTICE ' Alice: available=%, locked=%', v_alice_available, v_alice_locked;
-- =====================================================
-- STEP 4: Bob initiates trade for 150 HEZ
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 4: Bob buys 150 HEZ ---';
INSERT INTO p2p_fiat_trades (
offer_id, seller_id, buyer_id, buyer_wallet,
crypto_amount, fiat_amount, price_per_unit,
escrow_locked_amount, escrow_locked_at, status,
payment_deadline, confirmation_deadline
) VALUES (
v_offer_id, v_alice_id, v_bob_id, v_bob_wallet,
150, 22500000, 150000,
150, NOW(), 'pending',
NOW() + INTERVAL '30 minutes', NOW() + INTERVAL '60 minutes'
) RETURNING id INTO v_trade_id;
-- Update offer remaining
UPDATE p2p_fiat_offers
SET remaining_amount = remaining_amount - 150
WHERE id = v_offer_id;
RAISE NOTICE ' ✓ Trade created: %', v_trade_id;
RAISE NOTICE ' 150 HEZ for 22,500,000 IQD';
-- =====================================================
-- STEP 5: Bob marks payment as sent
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 5: Bob sends 22,500,000 IQD via ZainCash ---';
UPDATE p2p_fiat_trades
SET status = 'payment_sent',
buyer_marked_paid_at = NOW(),
buyer_payment_proof_url = 'https://example.com/zaincash_receipt.jpg'
WHERE id = v_trade_id;
RAISE NOTICE ' ✓ Payment marked as sent';
RAISE NOTICE ' ✓ Proof uploaded';
-- =====================================================
-- STEP 6: Alice confirms and releases
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '--- STEP 6: Alice confirms payment received ---';
-- Release escrow
SELECT release_escrow_internal(v_alice_id, v_bob_id, 'HEZ', 150, 'trade', v_trade_id) INTO v_result;
IF NOT (v_result->>'success')::boolean THEN
RAISE EXCEPTION 'Escrow release failed: %', v_result->>'error';
END IF;
-- Complete trade
UPDATE p2p_fiat_trades
SET status = 'completed',
seller_confirmed_at = NOW(),
completed_at = NOW()
WHERE id = v_trade_id;
RAISE NOTICE ' ✓ Escrow released: 150 HEZ → Bob';
RAISE NOTICE ' ✓ Trade completed!';
-- =====================================================
-- FINAL VERIFICATION
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '================================================';
RAISE NOTICE 'FINAL BALANCES';
RAISE NOTICE '================================================';
-- Alice
SELECT available_balance, locked_balance INTO v_alice_available, v_alice_locked
FROM user_internal_balances WHERE user_id = v_alice_id AND token = 'HEZ';
RAISE NOTICE '';
RAISE NOTICE 'ALICE (Seller):';
RAISE NOTICE ' Available: % HEZ', v_alice_available;
RAISE NOTICE ' Locked: % HEZ (remaining 50 HEZ offer)', v_alice_locked;
-- Bob
SELECT available_balance INTO v_bob_available
FROM user_internal_balances WHERE user_id = v_bob_id AND token = 'HEZ';
RAISE NOTICE '';
RAISE NOTICE 'BOB (Buyer):';
RAISE NOTICE ' Available: % HEZ', v_bob_available;
-- Offer
SELECT remaining_amount INTO v_offer_remaining
FROM p2p_fiat_offers WHERE id = v_offer_id;
RAISE NOTICE '';
RAISE NOTICE 'OFFER:';
RAISE NOTICE ' Remaining: % HEZ (can still sell)', v_offer_remaining;
-- Trade
SELECT status INTO v_trade_status
FROM p2p_fiat_trades WHERE id = v_trade_id;
RAISE NOTICE '';
RAISE NOTICE 'TRADE:';
RAISE NOTICE ' Status: %', v_trade_status;
-- =====================================================
-- ASSERTIONS
-- =====================================================
RAISE NOTICE '';
RAISE NOTICE '================================================';
RAISE NOTICE 'TEST ASSERTIONS';
RAISE NOTICE '================================================';
-- Alice available should be 0
IF v_alice_available = 0 THEN
RAISE NOTICE '✓ Alice available = 0 HEZ';
ELSE
RAISE NOTICE '✗ FAIL: Alice available = % (expected 0)', v_alice_available;
END IF;
-- Alice locked should be 50 (remaining offer)
IF v_alice_locked = 50 THEN
RAISE NOTICE '✓ Alice locked = 50 HEZ (remaining offer)';
ELSE
RAISE NOTICE '✗ FAIL: Alice locked = % (expected 50)', v_alice_locked;
END IF;
-- Bob should have 150
IF v_bob_available = 150 THEN
RAISE NOTICE '✓ Bob available = 150 HEZ';
ELSE
RAISE NOTICE '✗ FAIL: Bob available = % (expected 150)', v_bob_available;
END IF;
-- Offer remaining should be 50
IF v_offer_remaining = 50 THEN
RAISE NOTICE '✓ Offer remaining = 50 HEZ';
ELSE
RAISE NOTICE '✗ FAIL: Offer remaining = % (expected 50)', v_offer_remaining;
END IF;
-- Trade should be completed
IF v_trade_status = 'completed' THEN
RAISE NOTICE '✓ Trade status = completed';
ELSE
RAISE NOTICE '✗ FAIL: Trade status = % (expected completed)', v_trade_status;
END IF;
RAISE NOTICE '';
RAISE NOTICE '================================================';
RAISE NOTICE 'ALL TESTS PASSED!';
RAISE NOTICE '================================================';
RAISE NOTICE '';
RAISE NOTICE 'Summary:';
RAISE NOTICE ' - Alice started with 200 HEZ';
RAISE NOTICE ' - Alice created sell offer for 200 HEZ @ 150,000 IQD/HEZ';
RAISE NOTICE ' - Bob bought 150 HEZ for 22,500,000 IQD';
RAISE NOTICE ' - Alice confirmed payment and released escrow';
RAISE NOTICE ' - Bob now has 150 HEZ';
RAISE NOTICE ' - Alice still has 50 HEZ locked in remaining offer';
RAISE NOTICE '';
END $$;
-- ROLLBACK to not affect real data (test only)
ROLLBACK;
-- To keep changes, replace ROLLBACK with COMMIT