Files
defi-arbitrage/db/migrations/005_post_ledger_entry.sql
T
2026-03-02 12:14:07 -08:00

137 lines
4.0 KiB
PL/PgSQL

-- 005_post_ledger_entry.sql
-- Atomic ledger posting function with balance updates
-- Enforces idempotency, hash chaining, and balance integrity at DB level
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE OR REPLACE FUNCTION post_ledger_entry(
p_ledger_id text,
p_debit_account_id text,
p_credit_account_id text,
p_amount numeric,
p_currency_code text,
p_asset_type text,
p_transaction_type text,
p_reference_id text,
p_fx_rate numeric DEFAULT NULL,
p_metadata jsonb DEFAULT NULL
) RETURNS TABLE(
entry_id text,
block_hash text,
debit_balance numeric,
credit_balance numeric
) AS $$
DECLARE
v_entry_id text := gen_random_uuid()::text;
v_prev_hash text;
v_now timestamptz := now();
v_payload text;
v_block_hash text;
v_debit record;
v_credit record;
a1 text;
a2 text;
BEGIN
-- Validate amount
IF p_amount IS NULL OR p_amount <= 0 THEN
RAISE EXCEPTION 'Amount must be > 0';
END IF;
-- Idempotency check
IF EXISTS (
SELECT 1 FROM ledger_entries
WHERE ledger_id = p_ledger_id
AND reference_id = p_reference_id
) THEN
RAISE EXCEPTION 'Duplicate reference_id for ledger: %', p_reference_id;
END IF;
-- Lock ledger stream (prevents hash-chain races)
PERFORM pg_advisory_xact_lock(hashtext(p_ledger_id));
-- Deadlock-safe lock ordering (always lock accounts in id order)
a1 := LEAST(p_debit_account_id, p_credit_account_id);
a2 := GREATEST(p_debit_account_id, p_credit_account_id);
PERFORM 1 FROM bank_accounts WHERE id = a1 FOR UPDATE;
PERFORM 1 FROM bank_accounts WHERE id = a2 FOR UPDATE;
-- Fetch accounts (already locked)
SELECT * INTO v_debit FROM bank_accounts WHERE id = p_debit_account_id;
SELECT * INTO v_credit FROM bank_accounts WHERE id = p_credit_account_id;
IF v_debit.id IS NULL OR v_credit.id IS NULL THEN
RAISE EXCEPTION 'Account not found';
END IF;
-- Currency validation
IF v_debit.currency_code <> p_currency_code OR v_credit.currency_code <> p_currency_code THEN
RAISE EXCEPTION 'Currency mismatch';
END IF;
-- Sufficient funds check
IF v_debit.available_balance < p_amount THEN
RAISE EXCEPTION 'Insufficient balance: available=%, requested=%',
v_debit.available_balance, p_amount;
END IF;
-- Get previous hash for chain
SELECT block_hash INTO v_prev_hash
FROM ledger_entries
WHERE ledger_id = p_ledger_id
ORDER BY timestamp_utc DESC
LIMIT 1;
-- Compute canonical payload for block hash
v_payload :=
COALESCE(v_prev_hash,'') || '|' ||
v_entry_id || '|' ||
p_ledger_id || '|' ||
p_debit_account_id || '|' ||
p_credit_account_id || '|' ||
p_amount::text || '|' ||
p_currency_code || '|' ||
p_asset_type || '|' ||
p_transaction_type || '|' ||
p_reference_id || '|' ||
v_now::text;
-- Compute block hash
v_block_hash := encode(digest(v_payload, 'sha256'), 'hex');
-- Insert ledger entry
INSERT INTO ledger_entries (
id, ledger_id, debit_account_id, credit_account_id,
amount, currency_code, fx_rate, asset_type, transaction_type,
reference_id, timestamp_utc, block_hash, previous_hash,
status, metadata, created_at, updated_at
) VALUES (
v_entry_id, p_ledger_id, p_debit_account_id, p_credit_account_id,
p_amount, p_currency_code, p_fx_rate, p_asset_type, p_transaction_type,
p_reference_id, v_now, v_block_hash, v_prev_hash,
'POSTED', p_metadata, v_now, v_now
);
-- Update balances atomically
UPDATE bank_accounts
SET balance = balance - p_amount,
available_balance = available_balance - p_amount,
updated_at = v_now
WHERE id = p_debit_account_id;
UPDATE bank_accounts
SET balance = balance + p_amount,
available_balance = available_balance + p_amount,
updated_at = v_now
WHERE id = p_credit_account_id;
-- Return result
RETURN QUERY
SELECT
v_entry_id,
v_block_hash,
(SELECT balance FROM bank_accounts WHERE id = p_debit_account_id),
(SELECT balance FROM bank_accounts WHERE id = p_credit_account_id);
END;
$$ LANGUAGE plpgsql;