feat(gl): add immutable postgres ledger storage
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
CREATE TABLE IF NOT EXISTS ledger_schema_migrations (
|
||||
version bigint PRIMARY KEY,
|
||||
applied_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
||||
);
|
||||
|
||||
CREATE TABLE ledger_accounts (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
class text NOT NULL CHECK (class IN (
|
||||
'USER_AVAILABLE',
|
||||
'USER_FROZEN',
|
||||
'EXTERNAL_BLOCKCHAIN',
|
||||
'TREASURY',
|
||||
'MARKET_CLEARING',
|
||||
'IPG_CLEARING',
|
||||
'COMMISSION_REVENUE'
|
||||
)),
|
||||
owner_type text NOT NULL DEFAULT '',
|
||||
owner_id text NOT NULL DEFAULT '',
|
||||
asset_id bigint NOT NULL CHECK (asset_id > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (class, owner_type, owner_id, asset_id),
|
||||
UNIQUE (id, asset_id),
|
||||
CHECK (
|
||||
class NOT IN ('USER_AVAILABLE', 'USER_FROZEN')
|
||||
OR (owner_type <> '' AND owner_id <> '')
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE journals (
|
||||
id uuid PRIMARY KEY,
|
||||
source_service text NOT NULL CHECK (source_service <> ''),
|
||||
idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''),
|
||||
source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''),
|
||||
tracking_code text NOT NULL DEFAULT '',
|
||||
effect_kind text NOT NULL CHECK (effect_kind <> ''),
|
||||
event_version integer NOT NULL CHECK (event_version > 0),
|
||||
reversal_of_journal_id uuid REFERENCES journals (id),
|
||||
occurred_at timestamptz NOT NULL,
|
||||
recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
sealed_at timestamptz,
|
||||
correlation_id text NOT NULL DEFAULT '',
|
||||
actor_id text NOT NULL DEFAULT '',
|
||||
blockchain_network text NOT NULL DEFAULT '',
|
||||
blockchain_transaction_hash text NOT NULL DEFAULT '',
|
||||
blockchain_ledger_sequence text NOT NULL DEFAULT '',
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
|
||||
payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
|
||||
CHECK (reversal_of_journal_id IS NULL OR reversal_of_journal_id <> id)
|
||||
);
|
||||
|
||||
CREATE INDEX journals_source_transaction_idx
|
||||
ON journals (source_service, source_transaction_id);
|
||||
|
||||
CREATE INDEX journals_recorded_at_idx ON journals (recorded_at, id);
|
||||
|
||||
CREATE TABLE journal_entries (
|
||||
journal_id uuid NOT NULL REFERENCES journals (id),
|
||||
line_number integer NOT NULL CHECK (line_number > 0),
|
||||
account_id bigint NOT NULL,
|
||||
asset_id bigint NOT NULL CHECK (asset_id > 0),
|
||||
amount numeric(38, 18) NOT NULL CHECK (amount <> 0),
|
||||
description text NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (journal_id, line_number),
|
||||
FOREIGN KEY (account_id, asset_id) REFERENCES ledger_accounts (id, asset_id)
|
||||
);
|
||||
|
||||
CREATE INDEX journal_entries_account_idx
|
||||
ON journal_entries (account_id, journal_id, line_number);
|
||||
|
||||
CREATE TABLE transaction_events (
|
||||
id uuid PRIMARY KEY,
|
||||
source_service text NOT NULL CHECK (source_service <> ''),
|
||||
idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''),
|
||||
source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''),
|
||||
tracking_code text NOT NULL DEFAULT '',
|
||||
event_version integer NOT NULL CHECK (event_version > 0),
|
||||
state text NOT NULL CHECK (state IN (
|
||||
'CREATED',
|
||||
'PENDING_TRANSACTION',
|
||||
'PENDING_ADMIN',
|
||||
'SUCCESSFUL',
|
||||
'FAILED',
|
||||
'SUSPENDED'
|
||||
)),
|
||||
error_code text NOT NULL DEFAULT '',
|
||||
error_message text NOT NULL DEFAULT '',
|
||||
occurred_at timestamptz NOT NULL,
|
||||
recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
correlation_id text NOT NULL DEFAULT '',
|
||||
actor_id text NOT NULL DEFAULT '',
|
||||
blockchain_network text NOT NULL DEFAULT '',
|
||||
blockchain_transaction_hash text NOT NULL DEFAULT '',
|
||||
blockchain_ledger_sequence text NOT NULL DEFAULT '',
|
||||
metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'),
|
||||
payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$')
|
||||
);
|
||||
|
||||
CREATE INDEX transaction_events_source_idx
|
||||
ON transaction_events (source_service, source_transaction_id, event_version);
|
||||
|
||||
CREATE FUNCTION reject_ledger_mutation() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION '% is append-only', TG_TABLE_NAME
|
||||
USING ERRCODE = '55000';
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION guard_journal_seal() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF OLD.sealed_at IS NOT NULL
|
||||
OR NEW.sealed_at IS NULL
|
||||
OR (to_jsonb(NEW) - 'sealed_at') IS DISTINCT FROM (to_jsonb(OLD) - 'sealed_at') THEN
|
||||
RAISE EXCEPTION 'journals are immutable except for initial sealing'
|
||||
USING ERRCODE = '55000';
|
||||
END IF;
|
||||
|
||||
IF (SELECT count(*) FROM journal_entries WHERE journal_id = NEW.id) < 2 THEN
|
||||
RAISE EXCEPTION 'journal requires at least two entries'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT asset_id
|
||||
FROM journal_entries
|
||||
WHERE journal_id = NEW.id
|
||||
GROUP BY asset_id
|
||||
HAVING sum(amount) <> 0
|
||||
) THEN
|
||||
RAISE EXCEPTION 'journal is not balanced per asset'
|
||||
USING ERRCODE = '23514';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION guard_entry_insert() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF (SELECT sealed_at IS NOT NULL FROM journals WHERE id = NEW.journal_id) THEN
|
||||
RAISE EXCEPTION 'cannot append to a sealed journal'
|
||||
USING ERRCODE = '55000';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER journals_guard_update
|
||||
BEFORE UPDATE ON journals
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_journal_seal();
|
||||
|
||||
CREATE TRIGGER journals_reject_delete
|
||||
BEFORE DELETE ON journals
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
||||
|
||||
CREATE TRIGGER entries_guard_insert
|
||||
BEFORE INSERT ON journal_entries
|
||||
FOR EACH ROW EXECUTE FUNCTION guard_entry_insert();
|
||||
|
||||
CREATE TRIGGER entries_reject_update_or_delete
|
||||
BEFORE UPDATE OR DELETE ON journal_entries
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
||||
|
||||
CREATE TRIGGER accounts_reject_update_or_delete
|
||||
BEFORE UPDATE OR DELETE ON ledger_accounts
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
||||
|
||||
CREATE TRIGGER transaction_events_reject_update_or_delete
|
||||
BEFORE UPDATE OR DELETE ON transaction_events
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
|
||||
Reference in New Issue
Block a user