Files
pwap/web/supabase/migrations/017_withdraw_completion.sql
T
pezkuwichain 7636d46c7d feat(p2p): OKX-level security upgrade with Edge Functions
- Add process-withdraw Edge Function for blockchain withdrawals
- Update verify-deposit Edge Function with @pezkuwi/api
- Add withdrawal limits (daily/monthly) and fee system
- Add hot wallet configuration with production address
- Add admin roles for dispute resolution
- Add COMBINED SQL migration with full P2P system
- Encrypt payment details with AES-256-GCM
- Prevent TX hash reuse with UNIQUE constraint
2026-01-29 03:12:02 +03:00

212 lines
6.7 KiB
PL/PgSQL

-- =====================================================
-- Migration 017: Withdraw Completion Function
-- Properly handles successful withdrawal balance updates
-- =====================================================
-- Function to complete a withdrawal (called after blockchain TX success)
CREATE OR REPLACE FUNCTION complete_withdraw(
p_user_id UUID,
p_token TEXT,
p_amount DECIMAL(20, 12),
p_tx_hash TEXT,
p_request_id UUID
) RETURNS JSON AS $$
DECLARE
v_balance RECORD;
v_locked_before DECIMAL(20, 12);
BEGIN
-- Get current balance
SELECT * INTO v_balance
FROM user_internal_balances
WHERE user_id = p_user_id AND token = p_token
FOR UPDATE;
IF v_balance IS NULL THEN
RETURN json_build_object('success', false, 'error', 'Balance record not found');
END IF;
IF v_balance.locked_balance < p_amount THEN
RETURN json_build_object(
'success', false,
'error', 'Insufficient locked balance for withdrawal completion'
);
END IF;
v_locked_before := v_balance.locked_balance;
-- Deduct from locked balance (already deducted from available when request was made)
UPDATE user_internal_balances
SET
locked_balance = locked_balance - p_amount,
total_withdrawn = total_withdrawn + p_amount,
last_withdraw_at = NOW(),
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, 'withdraw', -p_amount,
v_locked_before, v_locked_before - p_amount, 'withdraw_request', p_request_id,
'Withdrawal completed. TX: ' || p_tx_hash
);
RETURN json_build_object(
'success', true,
'withdrawn_amount', p_amount,
'remaining_locked', v_locked_before - p_amount,
'tx_hash', p_tx_hash
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Grant execute to service role only (called from Edge Function)
REVOKE EXECUTE ON FUNCTION complete_withdraw FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION complete_withdraw FROM authenticated;
REVOKE EXECUTE ON FUNCTION complete_withdraw FROM anon;
-- Create index for faster withdrawal request lookups
CREATE INDEX IF NOT EXISTS idx_withdraw_requests_pending
ON p2p_deposit_withdraw_requests(user_id, status)
WHERE request_type = 'withdraw' AND status = 'pending';
-- Add fee tracking columns if not exists
ALTER TABLE p2p_deposit_withdraw_requests
ADD COLUMN IF NOT EXISTS fee_amount DECIMAL(20, 12) DEFAULT 0;
ALTER TABLE p2p_deposit_withdraw_requests
ADD COLUMN IF NOT EXISTS net_amount DECIMAL(20, 12);
-- Trigger to calculate net amount
CREATE OR REPLACE FUNCTION calculate_withdraw_net_amount()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.request_type = 'withdraw' THEN
-- Default fees: HEZ = 0.1, PEZ = 1
IF NEW.fee_amount IS NULL OR NEW.fee_amount = 0 THEN
NEW.fee_amount := CASE NEW.token
WHEN 'HEZ' THEN 0.1
WHEN 'PEZ' THEN 1
ELSE 0
END;
END IF;
NEW.net_amount := NEW.amount - NEW.fee_amount;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_calculate_withdraw_net ON p2p_deposit_withdraw_requests;
CREATE TRIGGER trigger_calculate_withdraw_net
BEFORE INSERT ON p2p_deposit_withdraw_requests
FOR EACH ROW EXECUTE FUNCTION calculate_withdraw_net_amount();
-- Add daily/monthly withdrawal limits table
CREATE TABLE IF NOT EXISTS p2p_withdrawal_limits (
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
daily_withdrawn DECIMAL(20, 12) DEFAULT 0,
monthly_withdrawn DECIMAL(20, 12) DEFAULT 0,
daily_limit DECIMAL(20, 12) DEFAULT 1000, -- Default 1000 HEZ/day
monthly_limit DECIMAL(20, 12) DEFAULT 10000, -- Default 10000 HEZ/month
last_daily_reset TIMESTAMPTZ DEFAULT NOW(),
last_monthly_reset TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Function to check withdrawal limits
CREATE OR REPLACE FUNCTION check_withdrawal_limit(
p_user_id UUID,
p_amount DECIMAL(20, 12)
) RETURNS JSON AS $$
DECLARE
v_limits RECORD;
v_daily_remaining DECIMAL;
v_monthly_remaining DECIMAL;
BEGIN
-- Get or create limits record
INSERT INTO p2p_withdrawal_limits (user_id)
VALUES (p_user_id)
ON CONFLICT (user_id) DO NOTHING;
SELECT * INTO v_limits
FROM p2p_withdrawal_limits
WHERE user_id = p_user_id
FOR UPDATE;
-- Reset daily counter if needed
IF v_limits.last_daily_reset < CURRENT_DATE THEN
UPDATE p2p_withdrawal_limits
SET daily_withdrawn = 0, last_daily_reset = NOW()
WHERE user_id = p_user_id;
v_limits.daily_withdrawn := 0;
END IF;
-- Reset monthly counter if needed
IF v_limits.last_monthly_reset < DATE_TRUNC('month', CURRENT_DATE) THEN
UPDATE p2p_withdrawal_limits
SET monthly_withdrawn = 0, last_monthly_reset = NOW()
WHERE user_id = p_user_id;
v_limits.monthly_withdrawn := 0;
END IF;
v_daily_remaining := v_limits.daily_limit - v_limits.daily_withdrawn;
v_monthly_remaining := v_limits.monthly_limit - v_limits.monthly_withdrawn;
IF p_amount > v_daily_remaining THEN
RETURN json_build_object(
'allowed', false,
'error', format('Daily limit exceeded. Remaining: %s', v_daily_remaining),
'daily_remaining', v_daily_remaining,
'monthly_remaining', v_monthly_remaining
);
END IF;
IF p_amount > v_monthly_remaining THEN
RETURN json_build_object(
'allowed', false,
'error', format('Monthly limit exceeded. Remaining: %s', v_monthly_remaining),
'daily_remaining', v_daily_remaining,
'monthly_remaining', v_monthly_remaining
);
END IF;
RETURN json_build_object(
'allowed', true,
'daily_remaining', v_daily_remaining,
'monthly_remaining', v_monthly_remaining
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to record withdrawal in limits
CREATE OR REPLACE FUNCTION record_withdrawal_limit(
p_user_id UUID,
p_amount DECIMAL(20, 12)
) RETURNS void AS $$
BEGIN
UPDATE p2p_withdrawal_limits
SET
daily_withdrawn = daily_withdrawn + p_amount,
monthly_withdrawn = monthly_withdrawn + p_amount,
updated_at = NOW()
WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
GRANT EXECUTE ON FUNCTION check_withdrawal_limit TO authenticated;
GRANT EXECUTE ON FUNCTION record_withdrawal_limit TO authenticated;
-- RLS for withdrawal limits
ALTER TABLE p2p_withdrawal_limits ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own withdrawal limits"
ON p2p_withdrawal_limits FOR SELECT
USING (user_id = auth.uid());
COMMENT ON FUNCTION complete_withdraw IS 'Completes a withdrawal after blockchain TX success. Service role only.';
COMMENT ON FUNCTION check_withdrawal_limit IS 'Check if user can withdraw the specified amount based on daily/monthly limits.';