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..373fa6e
--- /dev/null
+++ b/src/app/api/treasury/transactions/route.ts
@@ -0,0 +1,128 @@
+import { NextResponse } from "next/server";
+
+import { writeAuditEvent } from "@/lib/audit";
+import { getAuthSession } from "@/lib/auth/session";
+import {
+ getRecentTreasuryTransactionTransfers,
+ syncTreasuryTransactions,
+} from "@/lib/treasury/transactions";
+
+const DEFAULT_RECENT_TRANSFER_LIMIT = 25;
+
+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")) ??
+ 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
- {formatAddress(account.address)}
-
+
+ {formatAddress(account.address)}
+
+ Address not configured @@ -278,18 +281,13 @@ export function TreasuryDashboard({ )} -
- {formatCurrency(account.totalUsd)} -
- {account.address ? ( -+ {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..8038013 --- /dev/null +++ b/src/lib/treasury/transactions.ts @@ -0,0 +1,606 @@ +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 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; + 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 lower(value: string) { + return value.toLowerCase(); +} + +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