From 0cd3170e14ca6d4a36ca4b3ffa2b77a3f3f833b0 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Wed, 3 Jun 2026 16:25:24 -0600
Subject: [PATCH 1/2] feat: add treasury transaction ingestion
---
.env.example | 2 +
drizzle/0006_shocking_franklin_storm.sql | 56 +
drizzle/meta/0006_snapshot.json | 2032 +++++++++++++++++
drizzle/meta/_journal.json | 7 +
src/app/api/treasury/transactions/route.ts | 80 +
.../treasury/treasury-dashboard.tsx | 28 +-
src/db/schema.ts | 95 +
src/lib/treasury/accounts.ts | 25 +
src/lib/treasury/transactions.ts | 566 +++++
9 files changed, 2876 insertions(+), 15 deletions(-)
create mode 100644 drizzle/0006_shocking_franklin_storm.sql
create mode 100644 drizzle/meta/0006_snapshot.json
create mode 100644 src/app/api/treasury/transactions/route.ts
create mode 100644 src/lib/treasury/transactions.ts
diff --git a/.env.example b/.env.example
index 31162fb..cda8ce2 100644
--- a/.env.example
+++ b/.env.example
@@ -20,3 +20,5 @@ ANGRY_DWARF_HAT_ID=
# External APIs
# Optional; public CoinGecko price endpoint is used when unset.
COINGECKO_API_KEY=
+# Optional; defaults to Safe's Gnosis Transaction Service endpoint.
+SAFE_TRANSACTION_SERVICE_URL=
diff --git a/drizzle/0006_shocking_franklin_storm.sql b/drizzle/0006_shocking_franklin_storm.sql
new file mode 100644
index 0000000..13817a7
--- /dev/null
+++ b/drizzle/0006_shocking_franklin_storm.sql
@@ -0,0 +1,56 @@
+CREATE TYPE "public"."treasury_transfer_direction" AS ENUM('inflow', 'outflow', 'internal');--> statement-breakpoint
+CREATE TABLE "treasury_transaction_transfers" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "treasury_transaction_id" uuid NOT NULL,
+ "treasury_account_id" uuid,
+ "transfer_id" text NOT NULL,
+ "direction" "treasury_transfer_direction" NOT NULL,
+ "transfer_type" text NOT NULL,
+ "account_address" text NOT NULL,
+ "chain_id" integer NOT NULL,
+ "tx_hash" text NOT NULL,
+ "executed_at" timestamp with time zone NOT NULL,
+ "from_address" text NOT NULL,
+ "to_address" text NOT NULL,
+ "token_address" text,
+ "asset_symbol" text NOT NULL,
+ "asset_name" text NOT NULL,
+ "decimals" integer NOT NULL,
+ "raw_amount" numeric(78, 0) NOT NULL,
+ "amount" numeric(36, 18) NOT NULL,
+ "usd_price" numeric(18, 8),
+ "usd_amount" numeric(18, 2),
+ "raw_metadata" jsonb,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "treasury_transactions" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "treasury_account_id" uuid,
+ "source" "ledger_source" NOT NULL,
+ "account_address" text NOT NULL,
+ "chain_id" integer NOT NULL,
+ "tx_hash" text NOT NULL,
+ "safe_transaction_hash" text,
+ "transaction_type" text NOT NULL,
+ "executed_at" timestamp with time zone NOT NULL,
+ "block_number" integer,
+ "raw_metadata" jsonb,
+ "imported_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "treasury_transaction_transfers" ADD CONSTRAINT "treasury_transaction_transfers_treasury_transaction_id_treasury_transactions_id_fk" FOREIGN KEY ("treasury_transaction_id") REFERENCES "public"."treasury_transactions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "treasury_transaction_transfers" ADD CONSTRAINT "treasury_transaction_transfers_treasury_account_id_treasury_accounts_id_fk" FOREIGN KEY ("treasury_account_id") REFERENCES "public"."treasury_accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "treasury_transactions" ADD CONSTRAINT "treasury_transactions_treasury_account_id_treasury_accounts_id_fk" FOREIGN KEY ("treasury_account_id") REFERENCES "public"."treasury_accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE UNIQUE INDEX "treasury_transaction_transfers_chain_transfer_unique" ON "treasury_transaction_transfers" USING btree ("chain_id",lower("account_address"),"transfer_id");--> statement-breakpoint
+CREATE INDEX "treasury_transaction_transfers_transaction_id_idx" ON "treasury_transaction_transfers" USING btree ("treasury_transaction_id");--> statement-breakpoint
+CREATE INDEX "treasury_transaction_transfers_treasury_account_id_idx" ON "treasury_transaction_transfers" USING btree ("treasury_account_id");--> statement-breakpoint
+CREATE INDEX "treasury_transaction_transfers_tx_hash_idx" ON "treasury_transaction_transfers" USING btree ("tx_hash");--> statement-breakpoint
+CREATE INDEX "treasury_transaction_transfers_executed_at_idx" ON "treasury_transaction_transfers" USING btree ("executed_at");--> statement-breakpoint
+CREATE UNIQUE INDEX "treasury_transactions_chain_account_tx_unique" ON "treasury_transactions" USING btree ("chain_id",lower("account_address"),lower("tx_hash"));--> statement-breakpoint
+CREATE INDEX "treasury_transactions_tx_hash_idx" ON "treasury_transactions" USING btree ("tx_hash");--> statement-breakpoint
+CREATE INDEX "treasury_transactions_account_executed_idx" ON "treasury_transactions" USING btree ("chain_id","account_address","executed_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "treasury_transactions_treasury_account_id_idx" ON "treasury_transactions" USING btree ("treasury_account_id");
diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json
new file mode 100644
index 0000000..3c6d7b8
--- /dev/null
+++ b/drizzle/meta/0006_snapshot.json
@@ -0,0 +1,2032 @@
+{
+ "id": "579ab073-3a14-490d-adf3-ce9c785df55b",
+ "prevId": "8eddacdc-134b-4d19-b9ce-71d3f8bc0041",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.app_users": {
+ "name": "app_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "wallet_address": {
+ "name": "wallet_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name_encrypted": {
+ "name": "display_name_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "app_users_wallet_address_unique": {
+ "name": "app_users_wallet_address_unique",
+ "columns": [
+ {
+ "expression": "lower(\"wallet_address\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_events": {
+ "name": "audit_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_wallet_address": {
+ "name": "actor_wallet_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "audit_action",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_table": {
+ "name": "subject_table",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_id": {
+ "name": "subject_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quarter_id": {
+ "name": "quarter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_events_subject_idx": {
+ "name": "audit_events_subject_idx",
+ "columns": [
+ {
+ "expression": "subject_table",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_actor_user_id_idx": {
+ "name": "audit_events_actor_user_id_idx",
+ "columns": [
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_quarter_id_idx": {
+ "name": "audit_events_quarter_id_idx",
+ "columns": [
+ {
+ "expression": "quarter_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_events_created_at_idx": {
+ "name": "audit_events_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_events_actor_user_id_app_users_id_fk": {
+ "name": "audit_events_actor_user_id_app_users_id_fk",
+ "tableFrom": "audit_events",
+ "tableTo": "app_users",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_events_quarter_id_quarters_id_fk": {
+ "name": "audit_events_quarter_id_quarters_id_fk",
+ "tableFrom": "audit_events",
+ "tableTo": "quarters",
+ "columnsFrom": [
+ "quarter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cleric_roles": {
+ "name": "cleric_roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "wallet_address": {
+ "name": "wallet_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "granted_by_user_id": {
+ "name": "granted_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_by_user_id": {
+ "name": "revoked_by_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "cleric_roles_wallet_address_idx": {
+ "name": "cleric_roles_wallet_address_idx",
+ "columns": [
+ {
+ "expression": "lower(\"wallet_address\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cleric_roles_active_idx": {
+ "name": "cleric_roles_active_idx",
+ "columns": [
+ {
+ "expression": "lower(\"wallet_address\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "revoked_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "cleric_roles_granted_by_user_id_app_users_id_fk": {
+ "name": "cleric_roles_granted_by_user_id_app_users_id_fk",
+ "tableFrom": "cleric_roles",
+ "tableTo": "app_users",
+ "columnsFrom": [
+ "granted_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "cleric_roles_revoked_by_user_id_app_users_id_fk": {
+ "name": "cleric_roles_revoked_by_user_id_app_users_id_fk",
+ "tableFrom": "cleric_roles",
+ "tableTo": "app_users",
+ "columnsFrom": [
+ "revoked_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entities": {
+ "name": "entities",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "entity_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_encrypted": {
+ "name": "name_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "website_encrypted": {
+ "name": "website_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes_encrypted": {
+ "name": "notes_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_member": {
+ "name": "is_member",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "entities_type_idx": {
+ "name": "entities_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entities_archived_at_idx": {
+ "name": "entities_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entity_addresses": {
+ "name": "entity_addresses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label_encrypted": {
+ "name": "label_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "entity_addresses_entity_id_idx": {
+ "name": "entity_addresses_entity_id_idx",
+ "columns": [
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_addresses_address_idx": {
+ "name": "entity_addresses_address_idx",
+ "columns": [
+ {
+ "expression": "address",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_addresses_chain_address_unique": {
+ "name": "entity_addresses_chain_address_unique",
+ "columns": [
+ {
+ "expression": "coalesce(\"chain_id\", -1)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"address\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "entity_addresses_entity_id_entities_id_fk": {
+ "name": "entity_addresses_entity_id_entities_id_fk",
+ "tableFrom": "entity_addresses",
+ "tableTo": "entities",
+ "columnsFrom": [
+ "entity_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ledger_entries": {
+ "name": "ledger_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "quarter_id": {
+ "name": "quarter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "ledger_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "ledger_category",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'uncategorized'"
+ },
+ "verification_status": {
+ "name": "verification_status",
+ "type": "verification_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'verified'"
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tx_hash": {
+ "name": "tx_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "treasury_account_id": {
+ "name": "treasury_account_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "asset_symbol": {
+ "name": "asset_symbol",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "asset_amount": {
+ "name": "asset_amount",
+ "type": "numeric(36, 18)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usd_amount": {
+ "name": "usd_amount",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counterparty_entity_id": {
+ "name": "counterparty_entity_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raid_id": {
+ "name": "raid_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes_encrypted": {
+ "name": "notes_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_metadata": {
+ "name": "source_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ledger_entries_quarter_id_idx": {
+ "name": "ledger_entries_quarter_id_idx",
+ "columns": [
+ {
+ "expression": "quarter_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ledger_entries_category_idx": {
+ "name": "ledger_entries_category_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ledger_entries_tx_hash_idx": {
+ "name": "ledger_entries_tx_hash_idx",
+ "columns": [
+ {
+ "expression": "tx_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ledger_entries_raid_id_idx": {
+ "name": "ledger_entries_raid_id_idx",
+ "columns": [
+ {
+ "expression": "raid_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ledger_entries_occurred_at_idx": {
+ "name": "ledger_entries_occurred_at_idx",
+ "columns": [
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ledger_entries_quarter_id_quarters_id_fk": {
+ "name": "ledger_entries_quarter_id_quarters_id_fk",
+ "tableFrom": "ledger_entries",
+ "tableTo": "quarters",
+ "columnsFrom": [
+ "quarter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "ledger_entries_treasury_account_id_treasury_accounts_id_fk": {
+ "name": "ledger_entries_treasury_account_id_treasury_accounts_id_fk",
+ "tableFrom": "ledger_entries",
+ "tableTo": "treasury_accounts",
+ "columnsFrom": [
+ "treasury_account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "ledger_entries_counterparty_entity_id_entities_id_fk": {
+ "name": "ledger_entries_counterparty_entity_id_entities_id_fk",
+ "tableFrom": "ledger_entries",
+ "tableTo": "entities",
+ "columnsFrom": [
+ "counterparty_entity_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "ledger_entries_raid_id_raids_id_fk": {
+ "name": "ledger_entries_raid_id_raids_id_fk",
+ "tableFrom": "ledger_entries",
+ "tableTo": "raids",
+ "columnsFrom": [
+ "raid_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.quarters": {
+ "name": "quarters",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "year": {
+ "name": "year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quarter": {
+ "name": "quarter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "starts_on": {
+ "name": "starts_on",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ends_on": {
+ "name": "ends_on",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "quarter_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reopened_at": {
+ "name": "reopened_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "quarters_year_quarter_unique": {
+ "name": "quarters_year_quarter_unique",
+ "columns": [
+ {
+ "expression": "year",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "quarter",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "quarters_status_idx": {
+ "name": "quarters_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.raids": {
+ "name": "raids",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "client_entity_id": {
+ "name": "client_entity_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_encrypted": {
+ "name": "name_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notes_encrypted": {
+ "name": "notes_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "raids_client_entity_id_idx": {
+ "name": "raids_client_entity_id_idx",
+ "columns": [
+ {
+ "expression": "client_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "raids_archived_at_idx": {
+ "name": "raids_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "raids_client_entity_id_entities_id_fk": {
+ "name": "raids_client_entity_id_entities_id_fk",
+ "tableFrom": "raids",
+ "tableTo": "entities",
+ "columnsFrom": [
+ "client_entity_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.treasury_accounts": {
+ "name": "treasury_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "name_encrypted": {
+ "name": "name_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "address": {
+ "name": "address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "treasury_account_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_dao_controlled": {
+ "name": "is_dao_controlled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "notes_encrypted": {
+ "name": "notes_encrypted",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "treasury_accounts_chain_address_unique": {
+ "name": "treasury_accounts_chain_address_unique",
+ "columns": [
+ {
+ "expression": "chain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "address",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_accounts_type_idx": {
+ "name": "treasury_accounts_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_accounts_archived_at_idx": {
+ "name": "treasury_accounts_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.treasury_balance_assets": {
+ "name": "treasury_balance_assets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "symbol": {
+ "name": "symbol",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decimals": {
+ "name": "decimals",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "raw_amount": {
+ "name": "raw_amount",
+ "type": "numeric(78, 0)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "balance": {
+ "name": "balance",
+ "type": "numeric(36, 18)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usd_price": {
+ "name": "usd_price",
+ "type": "numeric(18, 8)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usd_value": {
+ "name": "usd_value",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "treasury_balance_assets_snapshot_id_idx": {
+ "name": "treasury_balance_assets_snapshot_id_idx",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_balance_assets_snapshot_symbol_unique": {
+ "name": "treasury_balance_assets_snapshot_symbol_unique",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "symbol",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "treasury_balance_assets_snapshot_id_treasury_balance_snapshots_id_fk": {
+ "name": "treasury_balance_assets_snapshot_id_treasury_balance_snapshots_id_fk",
+ "tableFrom": "treasury_balance_assets",
+ "tableTo": "treasury_balance_snapshots",
+ "columnsFrom": [
+ "snapshot_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.treasury_balance_snapshots": {
+ "name": "treasury_balance_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "account_address": {
+ "name": "account_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "treasury_snapshot_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_usd": {
+ "name": "total_usd",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "treasury_balance_snapshots_chain_account_synced_idx": {
+ "name": "treasury_balance_snapshots_chain_account_synced_idx",
+ "columns": [
+ {
+ "expression": "chain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_address",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "synced_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_balance_snapshots_synced_at_idx": {
+ "name": "treasury_balance_snapshots_synced_at_idx",
+ "columns": [
+ {
+ "expression": "synced_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.treasury_transaction_transfers": {
+ "name": "treasury_transaction_transfers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "treasury_transaction_id": {
+ "name": "treasury_transaction_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "treasury_account_id": {
+ "name": "treasury_account_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transfer_id": {
+ "name": "transfer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "treasury_transfer_direction",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "transfer_type": {
+ "name": "transfer_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_address": {
+ "name": "account_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tx_hash": {
+ "name": "tx_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "executed_at": {
+ "name": "executed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_address": {
+ "name": "from_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to_address": {
+ "name": "to_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_address": {
+ "name": "token_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "asset_symbol": {
+ "name": "asset_symbol",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "asset_name": {
+ "name": "asset_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decimals": {
+ "name": "decimals",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "raw_amount": {
+ "name": "raw_amount",
+ "type": "numeric(78, 0)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "amount": {
+ "name": "amount",
+ "type": "numeric(36, 18)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usd_price": {
+ "name": "usd_price",
+ "type": "numeric(18, 8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usd_amount": {
+ "name": "usd_amount",
+ "type": "numeric(18, 2)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_metadata": {
+ "name": "raw_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "treasury_transaction_transfers_chain_transfer_unique": {
+ "name": "treasury_transaction_transfers_chain_transfer_unique",
+ "columns": [
+ {
+ "expression": "chain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"account_address\")",
+ "isExpression": true,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "transfer_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transaction_transfers_transaction_id_idx": {
+ "name": "treasury_transaction_transfers_transaction_id_idx",
+ "columns": [
+ {
+ "expression": "treasury_transaction_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transaction_transfers_treasury_account_id_idx": {
+ "name": "treasury_transaction_transfers_treasury_account_id_idx",
+ "columns": [
+ {
+ "expression": "treasury_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transaction_transfers_tx_hash_idx": {
+ "name": "treasury_transaction_transfers_tx_hash_idx",
+ "columns": [
+ {
+ "expression": "tx_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transaction_transfers_executed_at_idx": {
+ "name": "treasury_transaction_transfers_executed_at_idx",
+ "columns": [
+ {
+ "expression": "executed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "treasury_transaction_transfers_treasury_transaction_id_treasury_transactions_id_fk": {
+ "name": "treasury_transaction_transfers_treasury_transaction_id_treasury_transactions_id_fk",
+ "tableFrom": "treasury_transaction_transfers",
+ "tableTo": "treasury_transactions",
+ "columnsFrom": [
+ "treasury_transaction_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "treasury_transaction_transfers_treasury_account_id_treasury_accounts_id_fk": {
+ "name": "treasury_transaction_transfers_treasury_account_id_treasury_accounts_id_fk",
+ "tableFrom": "treasury_transaction_transfers",
+ "tableTo": "treasury_accounts",
+ "columnsFrom": [
+ "treasury_account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.treasury_transactions": {
+ "name": "treasury_transactions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "treasury_account_id": {
+ "name": "treasury_account_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "ledger_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_address": {
+ "name": "account_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chain_id": {
+ "name": "chain_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tx_hash": {
+ "name": "tx_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "safe_transaction_hash": {
+ "name": "safe_transaction_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "transaction_type": {
+ "name": "transaction_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "executed_at": {
+ "name": "executed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_number": {
+ "name": "block_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_metadata": {
+ "name": "raw_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "imported_at": {
+ "name": "imported_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "treasury_transactions_chain_account_tx_unique": {
+ "name": "treasury_transactions_chain_account_tx_unique",
+ "columns": [
+ {
+ "expression": "chain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"account_address\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tx_hash\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transactions_tx_hash_idx": {
+ "name": "treasury_transactions_tx_hash_idx",
+ "columns": [
+ {
+ "expression": "tx_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transactions_account_executed_idx": {
+ "name": "treasury_transactions_account_executed_idx",
+ "columns": [
+ {
+ "expression": "chain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_address",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "executed_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "treasury_transactions_treasury_account_id_idx": {
+ "name": "treasury_transactions_treasury_account_id_idx",
+ "columns": [
+ {
+ "expression": "treasury_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "treasury_transactions_treasury_account_id_treasury_accounts_id_fk": {
+ "name": "treasury_transactions_treasury_account_id_treasury_accounts_id_fk",
+ "tableFrom": "treasury_transactions",
+ "tableTo": "treasury_accounts",
+ "columnsFrom": [
+ "treasury_account_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.audit_action": {
+ "name": "audit_action",
+ "schema": "public",
+ "values": [
+ "create",
+ "update",
+ "delete",
+ "import",
+ "classify",
+ "publish",
+ "reopen",
+ "grant_role",
+ "revoke_role"
+ ]
+ },
+ "public.entity_type": {
+ "name": "entity_type",
+ "schema": "public",
+ "values": [
+ "client",
+ "provider",
+ "subcontractor"
+ ]
+ },
+ "public.ledger_category": {
+ "name": "ledger_category",
+ "schema": "public",
+ "values": [
+ "raid_revenue",
+ "subcontractor_payout",
+ "provider_expense",
+ "member_dues",
+ "ragequit",
+ "treasury_transfer",
+ "uncategorized"
+ ]
+ },
+ "public.ledger_source": {
+ "name": "ledger_source",
+ "schema": "public",
+ "values": [
+ "main_safe",
+ "side_vault",
+ "manual",
+ "bank_csv",
+ "dao_proposal"
+ ]
+ },
+ "public.quarter_status": {
+ "name": "quarter_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "ready_for_review",
+ "published",
+ "reopened"
+ ]
+ },
+ "public.treasury_account_type": {
+ "name": "treasury_account_type",
+ "schema": "public",
+ "values": [
+ "main_safe",
+ "side_vault",
+ "operator"
+ ]
+ },
+ "public.treasury_snapshot_status": {
+ "name": "treasury_snapshot_status",
+ "schema": "public",
+ "values": [
+ "pending_live_sync",
+ "synced",
+ "stale_syncing",
+ "partial",
+ "failed"
+ ]
+ },
+ "public.treasury_transfer_direction": {
+ "name": "treasury_transfer_direction",
+ "schema": "public",
+ "values": [
+ "inflow",
+ "outflow",
+ "internal"
+ ]
+ },
+ "public.verification_status": {
+ "name": "verification_status",
+ "schema": "public",
+ "values": [
+ "verified",
+ "unverified"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index d05316c..43b416d 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -43,6 +43,13 @@
"when": 1780518492589,
"tag": "0005_public_diamondback",
"breakpoints": true
+ },
+ {
+ "idx": 6,
+ "version": "7",
+ "when": 1780524457784,
+ "tag": "0006_shocking_franklin_storm",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/app/api/treasury/transactions/route.ts b/src/app/api/treasury/transactions/route.ts
new file mode 100644
index 0000000..aed0e51
--- /dev/null
+++ b/src/app/api/treasury/transactions/route.ts
@@ -0,0 +1,80 @@
+import { NextResponse } from "next/server";
+
+import { writeAuditEvent } from "@/lib/audit";
+import { getAuthSession } from "@/lib/auth/session";
+import {
+ getRecentTreasuryTransactions,
+ syncTreasuryTransactions,
+} from "@/lib/treasury/transactions";
+
+function getPositiveQueryInteger(value: string | null) {
+ if (!value) {
+ return undefined;
+ }
+
+ const parsed = Number(value);
+
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
+}
+
+async function requireAdminSession() {
+ const session = await getAuthSession();
+
+ if (!session.address || !session.permissions?.canAdmin) {
+ return null;
+ }
+
+ return session;
+}
+
+export async function GET(request: Request) {
+ const session = await requireAdminSession();
+
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const url = new URL(request.url);
+ const limit = getPositiveQueryInteger(url.searchParams.get("limit"));
+ const transfers = await getRecentTreasuryTransactions(limit);
+
+ return NextResponse.json({ transfers });
+}
+
+export async function POST(request: Request) {
+ const session = await requireAdminSession();
+
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ try {
+ const url = new URL(request.url);
+ const result = await syncTreasuryTransactions({
+ limit: getPositiveQueryInteger(url.searchParams.get("limit")),
+ maxPages: getPositiveQueryInteger(url.searchParams.get("maxPages")),
+ });
+
+ await writeAuditEvent({
+ action: "import",
+ actorWalletAddress: session.address,
+ metadata: {
+ accountCount: result.accounts.length,
+ importedTransactions: result.importedTransactions,
+ importedTransfers: result.importedTransfers,
+ scannedTransfers: result.scannedTransfers,
+ },
+ subjectTable: "treasury_transactions",
+ summary: "Synced treasury transactions",
+ });
+
+ return NextResponse.json(result);
+ } catch (error) {
+ console.error("Treasury transaction sync failed", error);
+
+ return NextResponse.json(
+ { error: "Treasury transaction sync failed" },
+ { status: 502 },
+ );
+ }
+}
diff --git a/src/components/treasury/treasury-dashboard.tsx b/src/components/treasury/treasury-dashboard.tsx
index f3530fc..63dda24 100644
--- a/src/components/treasury/treasury-dashboard.tsx
+++ b/src/components/treasury/treasury-dashboard.tsx
@@ -268,9 +268,12 @@ export function TreasuryDashboard({
{account.name}
{account.address ? (
-
- {formatAddress(account.address)}
-
+
+
+ {formatAddress(account.address)}
+
+
+
) : (
Address not configured
@@ -278,18 +281,13 @@ export function TreasuryDashboard({
)}
-
-
- {formatCurrency(account.totalUsd)}
-
- {account.address ? (
-
- ) : null}
-
+
+ {formatCurrency(account.totalUsd)}
+
))}
diff --git a/src/db/schema.ts b/src/db/schema.ts
index 0859990..7db0bea 100644
--- a/src/db/schema.ts
+++ b/src/db/schema.ts
@@ -64,6 +64,11 @@ export const treasurySnapshotStatusEnum = pgEnum("treasury_snapshot_status", [
"failed",
]);
+export const treasuryTransferDirectionEnum = pgEnum(
+ "treasury_transfer_direction",
+ ["inflow", "outflow", "internal"],
+);
+
export const auditActionEnum = pgEnum("audit_action", [
"create",
"update",
@@ -217,6 +222,96 @@ export const treasuryBalanceAssets = pgTable(
],
);
+export const treasuryTransactions = pgTable(
+ "treasury_transactions",
+ {
+ id: uuid("id").defaultRandom().primaryKey(),
+ treasuryAccountId: uuid("treasury_account_id").references(
+ () => treasuryAccounts.id,
+ { onDelete: "set null" },
+ ),
+ source: ledgerSourceEnum("source").notNull(),
+ accountAddress: text("account_address").notNull(),
+ chainId: integer("chain_id").notNull(),
+ txHash: text("tx_hash").notNull(),
+ safeTransactionHash: text("safe_transaction_hash"),
+ transactionType: text("transaction_type").notNull(),
+ executedAt: timestamp("executed_at", { withTimezone: true }).notNull(),
+ blockNumber: integer("block_number"),
+ rawMetadata: jsonb("raw_metadata"),
+ importedAt: timestamp("imported_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ ...timestamps,
+ },
+ (table) => [
+ uniqueIndex("treasury_transactions_chain_account_tx_unique").on(
+ table.chainId,
+ sql`lower(${table.accountAddress})`,
+ sql`lower(${table.txHash})`,
+ ),
+ index("treasury_transactions_tx_hash_idx").on(table.txHash),
+ index("treasury_transactions_account_executed_idx").on(
+ table.chainId,
+ table.accountAddress,
+ table.executedAt.desc(),
+ ),
+ index("treasury_transactions_treasury_account_id_idx").on(
+ table.treasuryAccountId,
+ ),
+ ],
+);
+
+export const treasuryTransactionTransfers = pgTable(
+ "treasury_transaction_transfers",
+ {
+ id: uuid("id").defaultRandom().primaryKey(),
+ treasuryTransactionId: uuid("treasury_transaction_id")
+ .notNull()
+ .references(() => treasuryTransactions.id, { onDelete: "cascade" }),
+ treasuryAccountId: uuid("treasury_account_id").references(
+ () => treasuryAccounts.id,
+ { onDelete: "set null" },
+ ),
+ transferId: text("transfer_id").notNull(),
+ direction: treasuryTransferDirectionEnum("direction").notNull(),
+ transferType: text("transfer_type").notNull(),
+ accountAddress: text("account_address").notNull(),
+ chainId: integer("chain_id").notNull(),
+ txHash: text("tx_hash").notNull(),
+ executedAt: timestamp("executed_at", { withTimezone: true }).notNull(),
+ fromAddress: text("from_address").notNull(),
+ toAddress: text("to_address").notNull(),
+ tokenAddress: text("token_address"),
+ assetSymbol: text("asset_symbol").notNull(),
+ assetName: text("asset_name").notNull(),
+ decimals: integer("decimals").notNull(),
+ rawAmount: numeric("raw_amount", { precision: 78, scale: 0 }).notNull(),
+ amount: numeric("amount", { precision: 36, scale: 18 }).notNull(),
+ usdPrice: numeric("usd_price", { precision: 18, scale: 8 }),
+ usdAmount: numeric("usd_amount", { precision: 18, scale: 2 }),
+ rawMetadata: jsonb("raw_metadata"),
+ ...timestamps,
+ },
+ (table) => [
+ uniqueIndex("treasury_transaction_transfers_chain_transfer_unique").on(
+ table.chainId,
+ sql`lower(${table.accountAddress})`,
+ table.transferId,
+ ),
+ index("treasury_transaction_transfers_transaction_id_idx").on(
+ table.treasuryTransactionId,
+ ),
+ index("treasury_transaction_transfers_treasury_account_id_idx").on(
+ table.treasuryAccountId,
+ ),
+ index("treasury_transaction_transfers_tx_hash_idx").on(table.txHash),
+ index("treasury_transaction_transfers_executed_at_idx").on(
+ table.executedAt,
+ ),
+ ],
+);
+
export const entities = pgTable(
"entities",
{
diff --git a/src/lib/treasury/accounts.ts b/src/lib/treasury/accounts.ts
index 3984cd7..df25cb2 100644
--- a/src/lib/treasury/accounts.ts
+++ b/src/lib/treasury/accounts.ts
@@ -155,3 +155,28 @@ export async function listActiveGnosisBalanceAccounts(): Promise<
type: account.type as EditableTreasuryAccountType,
}));
}
+
+export async function listActiveGnosisSideVaultAccounts(): Promise<
+ TreasuryBalanceAccountSource[]
+> {
+ const db = getDb();
+ const accounts = await db
+ .select()
+ .from(treasuryAccounts)
+ .where(
+ and(
+ isNull(treasuryAccounts.archivedAt),
+ eq(treasuryAccounts.chainId, gnosis.id),
+ eq(treasuryAccounts.type, "side_vault"),
+ ),
+ )
+ .orderBy(asc(treasuryAccounts.createdAt));
+
+ return accounts.map((account) => ({
+ id: account.id,
+ name: decryptField(account.nameEncrypted as EncryptedField),
+ address: getAddress(account.address),
+ chainId: account.chainId,
+ type: "side_vault",
+ }));
+}
diff --git a/src/lib/treasury/transactions.ts b/src/lib/treasury/transactions.ts
new file mode 100644
index 0000000..2fb46e6
--- /dev/null
+++ b/src/lib/treasury/transactions.ts
@@ -0,0 +1,566 @@
+import "server-only";
+
+import { and, eq, sql } from "drizzle-orm";
+import { formatUnits, getAddress, isAddress, type Address } from "viem";
+import { gnosis } from "viem/chains";
+
+import { getDb } from "@/db";
+import {
+ treasuryTransactionTransfers,
+ treasuryTransactions,
+} from "@/db/schema";
+import { listActiveGnosisSideVaultAccounts } from "@/lib/treasury/accounts";
+
+const DEFAULT_SAFE_TRANSACTION_SERVICE_URL =
+ "https://api.safe.global/tx-service/gnosis/api/v1";
+const DEFAULT_PAGE_LIMIT = 100;
+const DEFAULT_MAX_PAGES = 3;
+
+type TreasuryTransactionSource = {
+ id: string;
+ name: string;
+ address: Address;
+ chainId: typeof gnosis.id;
+ source: "main_safe" | "side_vault";
+ treasuryAccountId: string | null;
+};
+
+type SafeTransferTokenInfo = {
+ decimals?: number | null;
+ name?: string | null;
+ symbol?: string | null;
+};
+
+type SafeTransfer = {
+ blockNumber?: number | null;
+ executionDate?: string | null;
+ from?: string | null;
+ logIndex?: number | null;
+ safeTxHash?: string | null;
+ to?: string | null;
+ tokenAddress?: string | null;
+ tokenInfo?: SafeTransferTokenInfo | null;
+ transactionHash?: string | null;
+ transferId?: string | null;
+ type?: string | null;
+ value?: string | null;
+};
+
+type SafeTransfersResponse = {
+ next?: string | null;
+ results?: SafeTransfer[];
+};
+
+type NormalizedTransfer = {
+ amount: string;
+ assetName: string;
+ assetSymbol: string;
+ blockNumber: number | null;
+ decimals: number;
+ direction: "inflow" | "outflow" | "internal";
+ executedAt: Date;
+ fromAddress: Address;
+ rawAmount: string;
+ rawMetadata: SafeTransfer;
+ safeTransactionHash: string | null;
+ toAddress: Address;
+ tokenAddress: Address | null;
+ transferId: string;
+ transferType: string;
+ txHash: `0x${string}`;
+ usdAmount: string | null;
+ usdPrice: string | null;
+};
+
+export type TreasuryTransactionAccountSyncResult = {
+ accountAddress: Address;
+ accountName: string;
+ importedTransfers: number;
+ importedTransactions: number;
+ scannedTransfers: number;
+ source: "main_safe" | "side_vault";
+};
+
+export type TreasuryTransactionSyncResult = {
+ accounts: TreasuryTransactionAccountSyncResult[];
+ importedTransfers: number;
+ importedTransactions: number;
+ scannedTransfers: number;
+ syncedAt: string;
+};
+
+function getMainSafeAddress() {
+ const address = process.env.MAIN_SAFE_ADDRESS;
+
+ if (!address || !isAddress(address, { strict: false })) {
+ return null;
+ }
+
+ return getAddress(address);
+}
+
+function getSafeTransactionServiceUrl() {
+ return (
+ process.env.SAFE_TRANSACTION_SERVICE_URL ??
+ DEFAULT_SAFE_TRANSACTION_SERVICE_URL
+ ).replace(/\/$/, "");
+}
+
+function getPositiveInteger(value: number | undefined, fallback: number) {
+ if (!value || !Number.isInteger(value) || value <= 0) {
+ return fallback;
+ }
+
+ return value;
+}
+
+function normalizeHash(value: string | null | undefined) {
+ if (!value || !/^0x[a-fA-F0-9]{64}$/.test(value)) {
+ return null;
+ }
+
+ return value.toLowerCase() as `0x${string}`;
+}
+
+function normalizeAddress(value: string | null | undefined) {
+ if (!value || !isAddress(value, { strict: false })) {
+ return null;
+ }
+
+ return getAddress(value);
+}
+
+function getDirection({
+ accountAddress,
+ fromAddress,
+ toAddress,
+}: {
+ accountAddress: Address;
+ fromAddress: Address;
+ toAddress: Address;
+}): "inflow" | "outflow" | "internal" {
+ const account = accountAddress.toLowerCase();
+ const from = fromAddress.toLowerCase();
+ const to = toAddress.toLowerCase();
+
+ if (from === account && to === account) {
+ return "internal";
+ }
+
+ return to === account ? "inflow" : "outflow";
+}
+
+function getAssetMetadata(transfer: SafeTransfer) {
+ if (transfer.type === "ETHER_TRANSFER") {
+ return {
+ assetName: "Gnosis xDAI",
+ assetSymbol: "xDAI",
+ decimals: 18,
+ tokenAddress: null,
+ usdPrice: "1.00000000",
+ };
+ }
+
+ const tokenInfo = transfer.tokenInfo;
+ const decimals = tokenInfo?.decimals;
+ const normalizedDecimals =
+ typeof decimals === "number" && Number.isInteger(decimals) ? decimals : 18;
+
+ return {
+ assetName: tokenInfo?.name || tokenInfo?.symbol || "Token",
+ assetSymbol: tokenInfo?.symbol || "TOKEN",
+ decimals: normalizedDecimals,
+ tokenAddress: normalizeAddress(transfer.tokenAddress),
+ usdPrice:
+ tokenInfo?.symbol === "USDC" || tokenInfo?.symbol === "wxDAI"
+ ? "1.00000000"
+ : null,
+ };
+}
+
+function getUsdAmount({
+ amount,
+ usdPrice,
+}: {
+ amount: string;
+ usdPrice: string | null;
+}) {
+ if (!usdPrice) {
+ return null;
+ }
+
+ const usdAmount = Number(amount) * Number(usdPrice);
+
+ return Number.isFinite(usdAmount) ? usdAmount.toFixed(2) : null;
+}
+
+function getFallbackTransferId({
+ transfer,
+ txHash,
+}: {
+ transfer: SafeTransfer;
+ txHash: `0x${string}`;
+}) {
+ return [
+ "safe",
+ txHash,
+ transfer.type ?? "transfer",
+ transfer.logIndex ?? "no-log",
+ transfer.from ?? "unknown-from",
+ transfer.to ?? "unknown-to",
+ transfer.value ?? "0",
+ transfer.tokenAddress ?? "native",
+ ].join(":");
+}
+
+function normalizeTransfer({
+ account,
+ transfer,
+}: {
+ account: TreasuryTransactionSource;
+ transfer: SafeTransfer;
+}): NormalizedTransfer | null {
+ const txHash = normalizeHash(transfer.transactionHash);
+ const fromAddress = normalizeAddress(transfer.from);
+ const toAddress = normalizeAddress(transfer.to);
+ const rawAmount = transfer.value ?? "0";
+ const executedAt = transfer.executionDate
+ ? new Date(transfer.executionDate)
+ : null;
+
+ if (
+ !txHash ||
+ !fromAddress ||
+ !toAddress ||
+ !executedAt ||
+ Number.isNaN(executedAt.getTime()) ||
+ !/^\d+$/.test(rawAmount)
+ ) {
+ return null;
+ }
+
+ const asset = getAssetMetadata(transfer);
+ const amount = formatUnits(BigInt(rawAmount), asset.decimals);
+ const usdAmount = getUsdAmount({ amount, usdPrice: asset.usdPrice });
+
+ return {
+ amount,
+ assetName: asset.assetName,
+ assetSymbol: asset.assetSymbol,
+ blockNumber:
+ Number.isInteger(transfer.blockNumber) && transfer.blockNumber
+ ? transfer.blockNumber
+ : null,
+ decimals: asset.decimals,
+ direction: getDirection({
+ accountAddress: account.address,
+ fromAddress,
+ toAddress,
+ }),
+ executedAt,
+ fromAddress,
+ rawAmount,
+ rawMetadata: transfer,
+ safeTransactionHash: normalizeHash(transfer.safeTxHash),
+ toAddress,
+ tokenAddress: asset.tokenAddress,
+ transferId: transfer.transferId ?? getFallbackTransferId({ transfer, txHash }),
+ transferType: transfer.type ?? "TRANSFER",
+ txHash,
+ usdAmount,
+ usdPrice: asset.usdPrice,
+ };
+}
+
+function getTransfersUrl({
+ account,
+ limit,
+ offset,
+}: {
+ account: TreasuryTransactionSource;
+ limit: number;
+ offset: number;
+}) {
+ const url = new URL(
+ `${getSafeTransactionServiceUrl()}/safes/${account.address}/transfers/`,
+ );
+ url.searchParams.set("limit", String(limit));
+ url.searchParams.set("offset", String(offset));
+
+ return url;
+}
+
+async function fetchSafeTransfersPage({
+ account,
+ limit,
+ offset,
+}: {
+ account: TreasuryTransactionSource;
+ limit: number;
+ offset: number;
+}) {
+ const response = await fetch(getTransfersUrl({ account, limit, offset }), {
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Safe Transaction Service failed for ${account.name}: ${response.status}`,
+ );
+ }
+
+ return (await response.json()) as SafeTransfersResponse;
+}
+
+async function listTransactionSources(): Promise {
+ const mainSafeAddress = getMainSafeAddress();
+ const sideVaults = await listActiveGnosisSideVaultAccounts();
+ const sources: TreasuryTransactionSource[] = [];
+ const seen = new Set();
+
+ if (mainSafeAddress) {
+ sources.push({
+ id: "treasury",
+ name: "Treasury",
+ address: mainSafeAddress,
+ chainId: gnosis.id,
+ source: "main_safe",
+ treasuryAccountId: null,
+ });
+ seen.add(`${gnosis.id}:${mainSafeAddress.toLowerCase()}`);
+ }
+
+ for (const sideVault of sideVaults) {
+ const key = `${sideVault.chainId}:${sideVault.address.toLowerCase()}`;
+
+ if (seen.has(key)) {
+ continue;
+ }
+
+ sources.push({
+ id: sideVault.id,
+ name: sideVault.name,
+ address: sideVault.address,
+ chainId: gnosis.id,
+ source: "side_vault",
+ treasuryAccountId: sideVault.id,
+ });
+ seen.add(key);
+ }
+
+ return sources;
+}
+
+async function getOrCreateTransaction({
+ account,
+ transfer,
+}: {
+ account: TreasuryTransactionSource;
+ transfer: NormalizedTransfer;
+}) {
+ const db = getDb();
+ const [existingTransaction] = await db
+ .select({ id: treasuryTransactions.id })
+ .from(treasuryTransactions)
+ .where(
+ and(
+ eq(treasuryTransactions.chainId, account.chainId),
+ eq(treasuryTransactions.accountAddress, account.address),
+ eq(treasuryTransactions.txHash, transfer.txHash),
+ ),
+ )
+ .limit(1);
+
+ if (existingTransaction) {
+ return { id: existingTransaction.id, inserted: false };
+ }
+
+ const [createdTransaction] = await db
+ .insert(treasuryTransactions)
+ .values({
+ accountAddress: account.address,
+ blockNumber: transfer.blockNumber,
+ chainId: account.chainId,
+ executedAt: transfer.executedAt,
+ rawMetadata: {
+ sourceAccountId: account.id,
+ },
+ safeTransactionHash: transfer.safeTransactionHash,
+ source: account.source,
+ transactionType: "safe_transfer",
+ treasuryAccountId: account.treasuryAccountId,
+ txHash: transfer.txHash,
+ })
+ .onConflictDoNothing()
+ .returning();
+
+ if (createdTransaction) {
+ return { id: createdTransaction.id, inserted: true };
+ }
+
+ const [transaction] = await db
+ .select({ id: treasuryTransactions.id })
+ .from(treasuryTransactions)
+ .where(
+ and(
+ eq(treasuryTransactions.chainId, account.chainId),
+ eq(treasuryTransactions.accountAddress, account.address),
+ eq(treasuryTransactions.txHash, transfer.txHash),
+ ),
+ )
+ .limit(1);
+
+ if (!transaction) {
+ throw new Error("Treasury transaction insert failed");
+ }
+
+ return { id: transaction.id, inserted: false };
+}
+
+async function insertTransfer({
+ account,
+ transfer,
+ transactionId,
+}: {
+ account: TreasuryTransactionSource;
+ transfer: NormalizedTransfer;
+ transactionId: string;
+}) {
+ const [createdTransfer] = await getDb()
+ .insert(treasuryTransactionTransfers)
+ .values({
+ accountAddress: account.address,
+ amount: transfer.amount,
+ assetName: transfer.assetName,
+ assetSymbol: transfer.assetSymbol,
+ chainId: account.chainId,
+ decimals: transfer.decimals,
+ direction: transfer.direction,
+ executedAt: transfer.executedAt,
+ fromAddress: transfer.fromAddress,
+ rawAmount: transfer.rawAmount,
+ rawMetadata: transfer.rawMetadata,
+ tokenAddress: transfer.tokenAddress,
+ toAddress: transfer.toAddress,
+ transferId: transfer.transferId,
+ transferType: transfer.transferType,
+ treasuryAccountId: account.treasuryAccountId,
+ treasuryTransactionId: transactionId,
+ txHash: transfer.txHash,
+ usdAmount: transfer.usdAmount,
+ usdPrice: transfer.usdPrice,
+ })
+ .onConflictDoNothing()
+ .returning();
+
+ return Boolean(createdTransfer);
+}
+
+async function syncAccountTransfers({
+ account,
+ limit,
+ maxPages,
+}: {
+ account: TreasuryTransactionSource;
+ limit: number;
+ maxPages: number;
+}): Promise {
+ let importedTransfers = 0;
+ let importedTransactions = 0;
+ let scannedTransfers = 0;
+
+ for (let page = 0; page < maxPages; page += 1) {
+ const offset = page * limit;
+ const body = await fetchSafeTransfersPage({ account, limit, offset });
+ const transfers = body.results ?? [];
+
+ if (transfers.length === 0) {
+ break;
+ }
+
+ for (const rawTransfer of transfers) {
+ const transfer = normalizeTransfer({ account, transfer: rawTransfer });
+
+ if (!transfer) {
+ continue;
+ }
+
+ scannedTransfers += 1;
+
+ const transaction = await getOrCreateTransaction({ account, transfer });
+ const didInsertTransfer = await insertTransfer({
+ account,
+ transactionId: transaction.id,
+ transfer,
+ });
+
+ if (transaction.inserted) {
+ importedTransactions += 1;
+ }
+
+ if (didInsertTransfer) {
+ importedTransfers += 1;
+ }
+ }
+
+ if (!body.next) {
+ break;
+ }
+ }
+
+ return {
+ accountAddress: account.address,
+ accountName: account.name,
+ importedTransfers,
+ importedTransactions,
+ scannedTransfers,
+ source: account.source,
+ };
+}
+
+export async function syncTreasuryTransactions({
+ limit,
+ maxPages,
+}: {
+ limit?: number;
+ maxPages?: number;
+} = {}): Promise {
+ const sources = await listTransactionSources();
+ const pageLimit = getPositiveInteger(limit, DEFAULT_PAGE_LIMIT);
+ const pageCount = getPositiveInteger(maxPages, DEFAULT_MAX_PAGES);
+
+ if (sources.length === 0) {
+ throw new Error("MAIN_SAFE_ADDRESS is required to sync treasury transactions");
+ }
+
+ const accounts = await Promise.all(
+ sources.map((account) =>
+ syncAccountTransfers({ account, limit: pageLimit, maxPages: pageCount }),
+ ),
+ );
+
+ return {
+ accounts,
+ importedTransfers: accounts.reduce(
+ (total, account) => total + account.importedTransfers,
+ 0,
+ ),
+ importedTransactions: accounts.reduce(
+ (total, account) => total + account.importedTransactions,
+ 0,
+ ),
+ scannedTransfers: accounts.reduce(
+ (total, account) => total + account.scannedTransfers,
+ 0,
+ ),
+ syncedAt: new Date().toISOString(),
+ };
+}
+
+export async function getRecentTreasuryTransactions(limit = 25) {
+ return getDb()
+ .select()
+ .from(treasuryTransactionTransfers)
+ .orderBy(sql`${treasuryTransactionTransfers.executedAt} desc`)
+ .limit(limit);
+}
From 066e1010e7690b71f14d075d0d96574ef96708b4 Mon Sep 17 00:00:00 2001
From: ECWireless
Date: Wed, 3 Jun 2026 16:46:26 -0600
Subject: [PATCH 2/2] fix: address transaction ingestion review
---
src/app/api/treasury/transactions/route.ts | 62 +++++++++++++++++++---
src/lib/treasury/transactions.ts | 54 ++++++++++++++++---
2 files changed, 102 insertions(+), 14 deletions(-)
diff --git a/src/app/api/treasury/transactions/route.ts b/src/app/api/treasury/transactions/route.ts
index aed0e51..373fa6e 100644
--- a/src/app/api/treasury/transactions/route.ts
+++ b/src/app/api/treasury/transactions/route.ts
@@ -3,10 +3,12 @@ import { NextResponse } from "next/server";
import { writeAuditEvent } from "@/lib/audit";
import { getAuthSession } from "@/lib/auth/session";
import {
- getRecentTreasuryTransactions,
+ getRecentTreasuryTransactionTransfers,
syncTreasuryTransactions,
} from "@/lib/treasury/transactions";
+const DEFAULT_RECENT_TRANSFER_LIMIT = 25;
+
function getPositiveQueryInteger(value: string | null) {
if (!value) {
return undefined;
@@ -35,12 +37,47 @@ export async function GET(request: Request) {
}
const url = new URL(request.url);
- const limit = getPositiveQueryInteger(url.searchParams.get("limit"));
- const transfers = await getRecentTreasuryTransactions(limit);
+ const limit =
+ getPositiveQueryInteger(url.searchParams.get("limit")) ??
+ DEFAULT_RECENT_TRANSFER_LIMIT;
+ const transfers = await getRecentTreasuryTransactionTransfers(limit);
return NextResponse.json({ transfers });
}
+function getErrorMetadata(error: unknown) {
+ if (error instanceof Error) {
+ return {
+ errorMessage: error.message,
+ errorStack: error.stack,
+ };
+ }
+
+ return { errorMessage: "Treasury transaction sync failed" };
+}
+
+async function writeTransactionSyncAuditEvent({
+ actorWalletAddress,
+ metadata,
+ summary,
+}: {
+ actorWalletAddress: string;
+ metadata: Record;
+ summary: string;
+}) {
+ try {
+ await writeAuditEvent({
+ action: "import",
+ actorWalletAddress,
+ metadata,
+ subjectTable: "treasury_transactions",
+ summary,
+ });
+ } catch (auditError) {
+ console.error("Failed to write treasury transaction sync audit event", auditError);
+ }
+}
+
export async function POST(request: Request) {
const session = await requireAdminSession();
@@ -48,6 +85,12 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
+ const actorWalletAddress = session.address;
+
+ if (!actorWalletAddress) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
try {
const url = new URL(request.url);
const result = await syncTreasuryTransactions({
@@ -55,16 +98,15 @@ export async function POST(request: Request) {
maxPages: getPositiveQueryInteger(url.searchParams.get("maxPages")),
});
- await writeAuditEvent({
- action: "import",
- actorWalletAddress: session.address,
+ await writeTransactionSyncAuditEvent({
+ actorWalletAddress,
metadata: {
accountCount: result.accounts.length,
+ errorCount: result.errors.length,
importedTransactions: result.importedTransactions,
importedTransfers: result.importedTransfers,
scannedTransfers: result.scannedTransfers,
},
- subjectTable: "treasury_transactions",
summary: "Synced treasury transactions",
});
@@ -72,6 +114,12 @@ export async function POST(request: Request) {
} catch (error) {
console.error("Treasury transaction sync failed", error);
+ await writeTransactionSyncAuditEvent({
+ actorWalletAddress,
+ metadata: getErrorMetadata(error),
+ summary: "Treasury transaction sync failed",
+ });
+
return NextResponse.json(
{ error: "Treasury transaction sync failed" },
{ status: 502 },
diff --git a/src/lib/treasury/transactions.ts b/src/lib/treasury/transactions.ts
index 2fb46e6..8038013 100644
--- a/src/lib/treasury/transactions.ts
+++ b/src/lib/treasury/transactions.ts
@@ -81,8 +81,16 @@ export type TreasuryTransactionAccountSyncResult = {
source: "main_safe" | "side_vault";
};
+export type TreasuryTransactionAccountSyncError = {
+ accountAddress: Address;
+ accountName: string;
+ error: string;
+ source: "main_safe" | "side_vault";
+};
+
export type TreasuryTransactionSyncResult = {
accounts: TreasuryTransactionAccountSyncResult[];
+ errors: TreasuryTransactionAccountSyncError[];
importedTransfers: number;
importedTransactions: number;
scannedTransfers: number;
@@ -122,6 +130,10 @@ function normalizeHash(value: string | null | undefined) {
return value.toLowerCase() as `0x${string}`;
}
+function lower(value: string) {
+ return value.toLowerCase();
+}
+
function normalizeAddress(value: string | null | undefined) {
if (!value || !isAddress(value, { strict: false })) {
return null;
@@ -365,8 +377,10 @@ async function getOrCreateTransaction({
.where(
and(
eq(treasuryTransactions.chainId, account.chainId),
- eq(treasuryTransactions.accountAddress, account.address),
- eq(treasuryTransactions.txHash, transfer.txHash),
+ sql`lower(${treasuryTransactions.accountAddress}) = ${lower(
+ account.address,
+ )}`,
+ sql`lower(${treasuryTransactions.txHash}) = ${lower(transfer.txHash)}`,
),
)
.limit(1);
@@ -404,8 +418,10 @@ async function getOrCreateTransaction({
.where(
and(
eq(treasuryTransactions.chainId, account.chainId),
- eq(treasuryTransactions.accountAddress, account.address),
- eq(treasuryTransactions.txHash, transfer.txHash),
+ sql`lower(${treasuryTransactions.accountAddress}) = ${lower(
+ account.address,
+ )}`,
+ sql`lower(${treasuryTransactions.txHash}) = ${lower(transfer.txHash)}`,
),
)
.limit(1);
@@ -530,17 +546,41 @@ export async function syncTreasuryTransactions({
const pageCount = getPositiveInteger(maxPages, DEFAULT_MAX_PAGES);
if (sources.length === 0) {
- throw new Error("MAIN_SAFE_ADDRESS is required to sync treasury transactions");
+ throw new Error(
+ "At least one treasury or active Gnosis side-vault account is required to sync treasury transactions",
+ );
}
- const accounts = await Promise.all(
+ const results = await Promise.allSettled(
sources.map((account) =>
syncAccountTransfers({ account, limit: pageLimit, maxPages: pageCount }),
),
);
+ const accounts: TreasuryTransactionAccountSyncResult[] = [];
+ const errors: TreasuryTransactionAccountSyncError[] = [];
+
+ results.forEach((result, index) => {
+ const source = sources[index];
+
+ if (result.status === "fulfilled") {
+ accounts.push(result.value);
+ return;
+ }
+
+ errors.push({
+ accountAddress: source.address,
+ accountName: source.name,
+ error:
+ result.reason instanceof Error
+ ? result.reason.message
+ : "Treasury account sync failed",
+ source: source.source,
+ });
+ });
return {
accounts,
+ errors,
importedTransfers: accounts.reduce(
(total, account) => total + account.importedTransfers,
0,
@@ -557,7 +597,7 @@ export async function syncTreasuryTransactions({
};
}
-export async function getRecentTreasuryTransactions(limit = 25) {
+export async function getRecentTreasuryTransactionTransfers(limit = 25) {
return getDb()
.select()
.from(treasuryTransactionTransfers)