mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-12 05:50:56 +00:00
e18ba679be
The migrations did not describe this database, and could not be made to.
Three findings, in the order they surfaced:
* 25 functions declared across six migrations — all recorded as applied — did
not exist. Their tables did. Two were reached by the app, so merchant tier
upgrades and post-trade reputation updates had been quietly dead. This only
came to light because a user hit "Could not find the function
public.upsert_user_profile(...)" while toggling a notification setting.
* admin_roles has three conflicting definitions across the set and production
matches none of them. 001 says (id, user_id, role, granted_by, granted_at),
COMBINED says (user_id, role, created_at), production has
(id, user_id, role, permissions, created_at, updated_at).
* Applied to an empty database, five migrations fail. The legacy 0NN filenames
sort before the 14-digit timestamps they depend on — "013" < "20241117054600"
— so 013 runs before the migration creating the table it alters. The set
could never have been replayed from scratch.
So it could not be tested, could not rebuild the database, and did not match what
was running. Widening or patching it would have been dressing up a history that
was already fiction.
The baseline is a pg_dump of the live public schema, which matches production by
construction. Privileges are included deliberately: the REVOKEs on
lock_escrow_internal, release_escrow_internal, refund_escrow_internal and
request_withdraw are the 20260725030000 hardening, and dropping them would hand
fund movement back to anon.
Verified step by step against production before committing:
- applies to an empty database cleanly, after three real obstacles were fixed
(extensions live in the `extensions` schema, supabase_admin membership,
platform-level ALTER DEFAULT PRIVILEGES that cannot apply outside Supabase)
- produces 83 tables / 39 functions / 222 indexes / 360 policies — identical
counts to production
- recorded as applied in supabase_migrations without touching the 37 existing
rows, then dry-run confirmed "up to date — no pending migrations", so the
next deploy will not try to replay it over live tables
- drift check now reports "every declared function is present"; the known-gaps
list drops from 22 entries to zero
The old files move to migrations/archive/ rather than being deleted — they are
the only record of why parts of this look the way they do. Their README says
plainly not to run them, and why.
CI now applies migrations to an empty Postgres on every PR. That check was
impossible while the old set was the starting point; it is the thing that stops
this class of drift from being discovered by a user again.
548 lines
18 KiB
PL/PgSQL
548 lines
18 KiB
PL/PgSQL
-- =====================================================
|
|
-- P2P INTERNAL LEDGER ESCROW SYSTEM (OKX Model)
|
|
-- Migration: 014_p2p_internal_ledger_escrow.sql
|
|
-- Date: 2025-12-11
|
|
-- =====================================================
|
|
--
|
|
-- This migration implements OKX-style internal ledger escrow where:
|
|
-- - Blockchain transactions ONLY occur at deposit/withdraw
|
|
-- - P2P trades use internal database balance transfers
|
|
-- - No blockchain transactions during actual P2P trading
|
|
--
|
|
|
|
-- =====================================================
|
|
-- 1. USER INTERNAL BALANCES TABLE
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS user_internal_balances (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL CHECK (token IN ('HEZ', 'PEZ')),
|
|
available_balance DECIMAL(20, 12) NOT NULL DEFAULT 0 CHECK (available_balance >= 0),
|
|
locked_balance DECIMAL(20, 12) NOT NULL DEFAULT 0 CHECK (locked_balance >= 0),
|
|
total_deposited DECIMAL(20, 12) NOT NULL DEFAULT 0,
|
|
total_withdrawn DECIMAL(20, 12) NOT NULL DEFAULT 0,
|
|
last_deposit_at TIMESTAMPTZ,
|
|
last_withdraw_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(user_id, token)
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_internal_balances_user ON user_internal_balances(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_internal_balances_token ON user_internal_balances(token);
|
|
|
|
-- RLS
|
|
ALTER TABLE user_internal_balances ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Users can view their own balances
|
|
CREATE POLICY "Users can view own balances"
|
|
ON user_internal_balances FOR SELECT
|
|
USING (user_id = auth.uid());
|
|
|
|
-- Only system can modify balances (via service role)
|
|
-- No INSERT/UPDATE/DELETE policies for regular users
|
|
|
|
-- Admins can view all balances
|
|
CREATE POLICY "Admins can view all balances"
|
|
ON user_internal_balances FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM profiles
|
|
WHERE profiles.id = auth.uid()
|
|
AND profiles.role IN ('admin', 'super_admin')
|
|
)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- 2. DEPOSIT/WITHDRAW REQUESTS TABLE
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS p2p_deposit_withdraw_requests (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
request_type TEXT NOT NULL CHECK (request_type IN ('deposit', 'withdraw')),
|
|
token TEXT NOT NULL CHECK (token IN ('HEZ', 'PEZ')),
|
|
amount DECIMAL(20, 12) NOT NULL CHECK (amount > 0),
|
|
wallet_address TEXT NOT NULL,
|
|
blockchain_tx_hash TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed', 'cancelled')),
|
|
processed_at TIMESTAMPTZ,
|
|
processed_by UUID REFERENCES auth.users(id),
|
|
error_message TEXT,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_deposit_withdraw_status ON p2p_deposit_withdraw_requests(status);
|
|
CREATE INDEX IF NOT EXISTS idx_deposit_withdraw_user ON p2p_deposit_withdraw_requests(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_deposit_withdraw_type ON p2p_deposit_withdraw_requests(request_type);
|
|
|
|
-- RLS
|
|
ALTER TABLE p2p_deposit_withdraw_requests ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Users can view their own requests
|
|
CREATE POLICY "Users can view own requests"
|
|
ON p2p_deposit_withdraw_requests FOR SELECT
|
|
USING (user_id = auth.uid());
|
|
|
|
-- Users can create their own requests
|
|
CREATE POLICY "Users can create own requests"
|
|
ON p2p_deposit_withdraw_requests FOR INSERT
|
|
WITH CHECK (user_id = auth.uid());
|
|
|
|
-- Admins can view and manage all requests
|
|
CREATE POLICY "Admins can manage all requests"
|
|
ON p2p_deposit_withdraw_requests FOR ALL
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM profiles
|
|
WHERE profiles.id = auth.uid()
|
|
AND profiles.role IN ('admin', 'super_admin')
|
|
)
|
|
);
|
|
|
|
-- =====================================================
|
|
-- 3. BALANCE TRANSACTION LOG (Audit Trail)
|
|
-- =====================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS p2p_balance_transactions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL,
|
|
transaction_type TEXT NOT NULL CHECK (transaction_type IN (
|
|
'deposit', 'withdraw', 'escrow_lock', 'escrow_release',
|
|
'escrow_refund', 'trade_receive', 'admin_adjustment'
|
|
)),
|
|
amount DECIMAL(20, 12) NOT NULL,
|
|
balance_before DECIMAL(20, 12) NOT NULL,
|
|
balance_after DECIMAL(20, 12) NOT NULL,
|
|
reference_type TEXT, -- 'offer', 'trade', 'deposit_request', 'withdraw_request'
|
|
reference_id UUID,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX IF NOT EXISTS idx_balance_tx_user ON p2p_balance_transactions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_balance_tx_type ON p2p_balance_transactions(transaction_type);
|
|
CREATE INDEX IF NOT EXISTS idx_balance_tx_created ON p2p_balance_transactions(created_at DESC);
|
|
|
|
-- RLS
|
|
ALTER TABLE p2p_balance_transactions ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Users can view their own transaction history
|
|
CREATE POLICY "Users can view own transactions"
|
|
ON p2p_balance_transactions FOR SELECT
|
|
USING (user_id = auth.uid());
|
|
|
|
-- =====================================================
|
|
-- 4. LOCK ESCROW FUNCTION (Internal Balance)
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION lock_escrow_internal(
|
|
p_user_id UUID,
|
|
p_token TEXT,
|
|
p_amount DECIMAL(20, 12),
|
|
p_reference_type TEXT DEFAULT NULL,
|
|
p_reference_id UUID DEFAULT NULL
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
v_balance RECORD;
|
|
v_balance_before DECIMAL(20, 12);
|
|
BEGIN
|
|
-- Lock user's balance row for update
|
|
SELECT * INTO v_balance
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_user_id AND token = p_token
|
|
FOR UPDATE;
|
|
|
|
-- Check if balance exists
|
|
IF v_balance IS NULL THEN
|
|
RETURN json_build_object(
|
|
'success', false,
|
|
'error', 'No balance found for token ' || p_token || '. Please deposit first.'
|
|
);
|
|
END IF;
|
|
|
|
v_balance_before := v_balance.available_balance;
|
|
|
|
-- Check sufficient balance
|
|
IF v_balance.available_balance < p_amount THEN
|
|
RETURN json_build_object(
|
|
'success', false,
|
|
'error', 'Insufficient balance. Available: ' || v_balance.available_balance || ' ' || p_token
|
|
);
|
|
END IF;
|
|
|
|
-- Move from available to locked
|
|
UPDATE user_internal_balances
|
|
SET
|
|
available_balance = available_balance - p_amount,
|
|
locked_balance = locked_balance + p_amount,
|
|
updated_at = NOW()
|
|
WHERE user_id = p_user_id AND token = p_token;
|
|
|
|
-- Log the transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_user_id, p_token, 'escrow_lock', p_amount,
|
|
v_balance_before, v_balance_before - p_amount, p_reference_type, p_reference_id,
|
|
'Escrow locked for P2P offer'
|
|
);
|
|
|
|
RETURN json_build_object(
|
|
'success', true,
|
|
'locked_amount', p_amount,
|
|
'available_balance', v_balance_before - p_amount,
|
|
'locked_balance', v_balance.locked_balance + p_amount
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 5. RELEASE ESCROW FUNCTION (Trade Completion)
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION release_escrow_internal(
|
|
p_from_user_id UUID,
|
|
p_to_user_id UUID,
|
|
p_token TEXT,
|
|
p_amount DECIMAL(20, 12),
|
|
p_reference_type TEXT DEFAULT 'trade',
|
|
p_reference_id UUID DEFAULT NULL
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
v_from_balance RECORD;
|
|
v_to_balance RECORD;
|
|
v_from_balance_before DECIMAL(20, 12);
|
|
v_to_balance_before DECIMAL(20, 12);
|
|
BEGIN
|
|
-- Lock seller's balance row
|
|
SELECT * INTO v_from_balance
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_from_user_id AND token = p_token
|
|
FOR UPDATE;
|
|
|
|
-- Check seller has sufficient locked balance
|
|
IF v_from_balance IS NULL OR v_from_balance.locked_balance < p_amount THEN
|
|
RETURN json_build_object(
|
|
'success', false,
|
|
'error', 'Insufficient locked balance for release'
|
|
);
|
|
END IF;
|
|
|
|
v_from_balance_before := v_from_balance.locked_balance;
|
|
|
|
-- Reduce seller's locked balance
|
|
UPDATE user_internal_balances
|
|
SET
|
|
locked_balance = locked_balance - p_amount,
|
|
updated_at = NOW()
|
|
WHERE user_id = p_from_user_id AND token = p_token;
|
|
|
|
-- Log seller's transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_from_user_id, p_token, 'escrow_release', -p_amount,
|
|
v_from_balance_before, v_from_balance_before - p_amount, p_reference_type, p_reference_id,
|
|
'Escrow released to buyer'
|
|
);
|
|
|
|
-- Get or initialize buyer's balance
|
|
SELECT available_balance INTO v_to_balance_before
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_to_user_id AND token = p_token;
|
|
|
|
IF v_to_balance_before IS NULL THEN
|
|
v_to_balance_before := 0;
|
|
END IF;
|
|
|
|
-- Increase buyer's available balance (upsert)
|
|
INSERT INTO user_internal_balances (user_id, token, available_balance)
|
|
VALUES (p_to_user_id, p_token, p_amount)
|
|
ON CONFLICT (user_id, token)
|
|
DO UPDATE SET
|
|
available_balance = user_internal_balances.available_balance + p_amount,
|
|
updated_at = NOW();
|
|
|
|
-- Log buyer's transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_to_user_id, p_token, 'trade_receive', p_amount,
|
|
v_to_balance_before, v_to_balance_before + p_amount, p_reference_type, p_reference_id,
|
|
'Received from P2P trade'
|
|
);
|
|
|
|
RETURN json_build_object(
|
|
'success', true,
|
|
'transferred_amount', p_amount,
|
|
'from_user_id', p_from_user_id,
|
|
'to_user_id', p_to_user_id
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 6. REFUND ESCROW FUNCTION (Trade Cancellation)
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION refund_escrow_internal(
|
|
p_user_id UUID,
|
|
p_token TEXT,
|
|
p_amount DECIMAL(20, 12),
|
|
p_reference_type TEXT DEFAULT 'trade',
|
|
p_reference_id UUID DEFAULT NULL
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
v_balance RECORD;
|
|
v_locked_before DECIMAL(20, 12);
|
|
BEGIN
|
|
-- Lock user's balance row
|
|
SELECT * INTO v_balance
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_user_id AND token = p_token
|
|
FOR UPDATE;
|
|
|
|
-- Check sufficient locked balance
|
|
IF v_balance IS NULL OR v_balance.locked_balance < p_amount THEN
|
|
RETURN json_build_object(
|
|
'success', false,
|
|
'error', 'Insufficient locked balance for refund'
|
|
);
|
|
END IF;
|
|
|
|
v_locked_before := v_balance.locked_balance;
|
|
|
|
-- Move from locked back to available
|
|
UPDATE user_internal_balances
|
|
SET
|
|
locked_balance = locked_balance - p_amount,
|
|
available_balance = available_balance + p_amount,
|
|
updated_at = NOW()
|
|
WHERE user_id = p_user_id AND token = p_token;
|
|
|
|
-- Log the transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_user_id, p_token, 'escrow_refund', p_amount,
|
|
v_locked_before, v_locked_before - p_amount, p_reference_type, p_reference_id,
|
|
'Escrow refunded (trade cancelled)'
|
|
);
|
|
|
|
RETURN json_build_object(
|
|
'success', true,
|
|
'refunded_amount', p_amount,
|
|
'available_balance', v_balance.available_balance + p_amount,
|
|
'locked_balance', v_locked_before - p_amount
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 7. PROCESS DEPOSIT FUNCTION
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION process_deposit(
|
|
p_user_id UUID,
|
|
p_token TEXT,
|
|
p_amount DECIMAL(20, 12),
|
|
p_tx_hash TEXT,
|
|
p_request_id UUID DEFAULT NULL
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
v_balance_before DECIMAL(20, 12) := 0;
|
|
BEGIN
|
|
-- Get current balance
|
|
SELECT available_balance INTO v_balance_before
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_user_id AND token = p_token;
|
|
|
|
IF v_balance_before IS NULL THEN
|
|
v_balance_before := 0;
|
|
END IF;
|
|
|
|
-- Upsert balance
|
|
INSERT INTO user_internal_balances (
|
|
user_id, token, available_balance, total_deposited, last_deposit_at
|
|
) VALUES (
|
|
p_user_id, p_token, p_amount, p_amount, NOW()
|
|
)
|
|
ON CONFLICT (user_id, token)
|
|
DO UPDATE SET
|
|
available_balance = user_internal_balances.available_balance + p_amount,
|
|
total_deposited = user_internal_balances.total_deposited + p_amount,
|
|
last_deposit_at = NOW(),
|
|
updated_at = NOW();
|
|
|
|
-- Log the transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_user_id, p_token, 'deposit', p_amount,
|
|
v_balance_before, v_balance_before + p_amount, 'deposit_request', p_request_id,
|
|
'Deposit from blockchain TX: ' || p_tx_hash
|
|
);
|
|
|
|
-- Update request status if provided
|
|
IF p_request_id IS NOT NULL THEN
|
|
UPDATE p2p_deposit_withdraw_requests
|
|
SET
|
|
status = 'completed',
|
|
blockchain_tx_hash = p_tx_hash,
|
|
processed_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = p_request_id;
|
|
END IF;
|
|
|
|
RETURN json_build_object(
|
|
'success', true,
|
|
'deposited_amount', p_amount,
|
|
'new_balance', v_balance_before + p_amount
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 8. REQUEST WITHDRAW FUNCTION
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION request_withdraw(
|
|
p_user_id UUID,
|
|
p_token TEXT,
|
|
p_amount DECIMAL(20, 12),
|
|
p_wallet_address TEXT
|
|
) RETURNS JSON AS $$
|
|
DECLARE
|
|
v_balance RECORD;
|
|
v_request_id UUID;
|
|
BEGIN
|
|
-- Lock user's balance
|
|
SELECT * INTO v_balance
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_user_id AND token = p_token
|
|
FOR UPDATE;
|
|
|
|
-- Check sufficient available balance
|
|
IF v_balance IS NULL OR v_balance.available_balance < p_amount THEN
|
|
RETURN json_build_object(
|
|
'success', false,
|
|
'error', 'Insufficient available balance. Available: ' || COALESCE(v_balance.available_balance, 0)
|
|
);
|
|
END IF;
|
|
|
|
-- Lock the amount (move to locked_balance)
|
|
UPDATE user_internal_balances
|
|
SET
|
|
available_balance = available_balance - p_amount,
|
|
locked_balance = locked_balance + p_amount,
|
|
updated_at = NOW()
|
|
WHERE user_id = p_user_id AND token = p_token;
|
|
|
|
-- Create withdrawal request
|
|
INSERT INTO p2p_deposit_withdraw_requests (
|
|
user_id, request_type, token, amount, wallet_address, status
|
|
) VALUES (
|
|
p_user_id, 'withdraw', p_token, p_amount, p_wallet_address, 'pending'
|
|
) RETURNING id INTO v_request_id;
|
|
|
|
-- Log the transaction
|
|
INSERT INTO p2p_balance_transactions (
|
|
user_id, token, transaction_type, amount,
|
|
balance_before, balance_after, reference_type, reference_id,
|
|
description
|
|
) VALUES (
|
|
p_user_id, p_token, 'withdraw', -p_amount,
|
|
v_balance.available_balance, v_balance.available_balance - p_amount, 'withdraw_request', v_request_id,
|
|
'Withdrawal request to ' || p_wallet_address
|
|
);
|
|
|
|
RETURN json_build_object(
|
|
'success', true,
|
|
'request_id', v_request_id,
|
|
'amount', p_amount,
|
|
'wallet_address', p_wallet_address,
|
|
'status', 'pending'
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 9. GET USER BALANCE FUNCTION
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION get_user_internal_balance(p_user_id UUID)
|
|
RETURNS JSON AS $$
|
|
DECLARE
|
|
v_balances JSON;
|
|
BEGIN
|
|
SELECT json_agg(
|
|
json_build_object(
|
|
'token', token,
|
|
'available_balance', available_balance,
|
|
'locked_balance', locked_balance,
|
|
'total_balance', available_balance + locked_balance,
|
|
'total_deposited', total_deposited,
|
|
'total_withdrawn', total_withdrawn
|
|
)
|
|
) INTO v_balances
|
|
FROM user_internal_balances
|
|
WHERE user_id = p_user_id;
|
|
|
|
RETURN COALESCE(v_balances, '[]'::json);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- =====================================================
|
|
-- 10. HELPER: UPDATE UPDATED_AT TRIGGER
|
|
-- =====================================================
|
|
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = NOW();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Apply trigger to tables
|
|
DROP TRIGGER IF EXISTS update_user_internal_balances_updated_at ON user_internal_balances;
|
|
CREATE TRIGGER update_user_internal_balances_updated_at
|
|
BEFORE UPDATE ON user_internal_balances
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
DROP TRIGGER IF EXISTS update_deposit_withdraw_requests_updated_at ON p2p_deposit_withdraw_requests;
|
|
CREATE TRIGGER update_deposit_withdraw_requests_updated_at
|
|
BEFORE UPDATE ON p2p_deposit_withdraw_requests
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
-- =====================================================
|
|
-- COMMENTS
|
|
-- =====================================================
|
|
|
|
COMMENT ON TABLE user_internal_balances IS 'User internal balances for P2P trading (OKX-style internal ledger)';
|
|
COMMENT ON TABLE p2p_deposit_withdraw_requests IS 'Requests for deposits/withdrawals that require blockchain transactions';
|
|
COMMENT ON TABLE p2p_balance_transactions IS 'Audit log of all balance changes';
|
|
COMMENT ON FUNCTION lock_escrow_internal IS 'Lock user balance for P2P escrow (internal ledger)';
|
|
COMMENT ON FUNCTION release_escrow_internal IS 'Release escrow to buyer on trade completion';
|
|
COMMENT ON FUNCTION refund_escrow_internal IS 'Refund escrow to seller on trade cancellation';
|
|
COMMENT ON FUNCTION process_deposit IS 'Credit user balance after deposit verification';
|
|
COMMENT ON FUNCTION request_withdraw IS 'Create withdrawal request and lock balance';
|