diff --git a/.github/workflows/migration-test.yml b/.github/workflows/migration-test.yml index d463203..fcfd9b0 100644 --- a/.github/workflows/migration-test.yml +++ b/.github/workflows/migration-test.yml @@ -6,7 +6,7 @@ on: jobs: migration-test: runs-on: ubuntu-latest - + services: postgres: image: postgres:15 @@ -21,7 +21,7 @@ jobs: --health-retries 5 ports: - 5432:5432 - + redis: image: redis:7 options: >- @@ -31,20 +31,20 @@ jobs: --health-retries 5 ports: - 6379:6379 - + steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Set up Go uses: actions/setup-go@v5 with: go-version: '1.24.2' - + - name: Download go-wrappers run: | git clone https://github.com/vultisig/go-wrappers.git ../go-wrappers - + - name: Create test config run: | cat > dca.json </dev/null; then echo "DCA process failed to start" exit 1 fi - + # Check if migrations ran successfully by verifying tables exist PGPASSWORD=mypassword psql -h localhost -U myuser -d vultisig-plugin -c "\dt" | grep -E "(plugin_policies|time_triggers|transaction_history) | wc -l" > /dev/null if [ $? -ne 0 ]; then @@ -113,12 +113,12 @@ jobs: kill $DCA_PID exit 1 fi - + echo "All migrations ran successfully!" - + # Stop dca kill $DCA_PID - + - name: Check migration integrity run: | # Verify goose migrations table exists and has entries @@ -127,5 +127,30 @@ jobs: echo "Goose migrations table not found or empty" exit 1 fi - - echo "Migration integrity check passed!" \ No newline at end of file + + echo "Migration integrity check passed!" + + - name: Check schema updates + run: | + # Dump current schema + PGPASSWORD=mypassword pg_dump -h localhost -U myuser -d vultisig-plugin \ + --schema-only \ + --no-comments \ + --no-owner \ + --quote-all-identifiers \ + -T public.goose_db_version \ + -T public.goose_db_version_id_seq | sed \ + -e '/^--.*/d' \ + -e '/^SET /d' \ + -e '/^SELECT pg_catalog./d' \ + -e 's/"public"\.//' | awk '/./ { e=0 } /^$$/ { e += 1 } e <= 1' \ + > ./current_schema.sql + + # Compare with repository schema + if ! diff -u ./storage/postgres/schema/schema.sql ./current_schema.sql; then + echo "Schema has changed but schema.sql was not updated!" + echo "Please run 'make dump-schema CONFIG=config-plugin.yaml' to update the schema file." + exit 1 + fi + + echo "Schema is up to date!" diff --git a/Makefile b/Makefile index f394d00..a59e451 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,24 @@ plugin-server: @DYLD_LIBRARY_PATH=$(DYLD_LIBRARY) VS_CONFIG_NAME=config-plugin go run cmd/vultisigner/main.go plugin-worker: - @DYLD_LIBRARY_PATH=$(DYLD_LIBRARY) VS_CONFIG_NAME=config-plugin go run cmd/worker/main.go \ No newline at end of file + @DYLD_LIBRARY_PATH=$(DYLD_LIBRARY) VS_CONFIG_NAME=config-plugin go run cmd/worker/main.go + +# Dump database schema +# Usage: make dump-schema CONFIG=config-plugin.yaml +dump-schema: + @if [ -z "$(CONFIG)" ]; then \ + echo "Error: CONFIG parameter is required. Usage: make dump-schema CONFIG=config-plugin.yaml"; \ + exit 1; \ + fi + @DSN=$$(yq eval '.database.dsn' $(CONFIG)); \ + pg_dump "$$DSN" --schema-only \ + --no-comments \ + --no-owner \ + --quote-all-identifiers \ + -T public.goose_db_version \ + -T public.goose_db_version_id_seq | sed \ + -e '/^--.*/d' \ + -e '/^SET /d' \ + -e '/^SELECT pg_catalog./d' \ + -e 's/"public"\.//' | awk '/./ { e=0 } /^$$/ { e += 1 } e <= 1' \ + > ./storage/postgres/schema/schema.sql \ No newline at end of file diff --git a/storage/postgres/schema/schema.sql b/storage/postgres/schema/schema.sql new file mode 100644 index 0000000..b391ad5 --- /dev/null +++ b/storage/postgres/schema/schema.sql @@ -0,0 +1,141 @@ + +CREATE TYPE "plugin_id" AS ENUM ( + 'vultisig-dca-0000', + 'vultisig-payroll-0000', + 'vultisig-fees-feee' +); + +CREATE TYPE "transaction_status" AS ENUM ( + 'PENDING', + 'SIGNING_FAILED', + 'SIGNED', + 'BROADCAST', + 'MINED', + 'REJECTED' +); + +CREATE TYPE "trigger_status" AS ENUM ( + 'PENDING', + 'RUNNING' +); + +CREATE TYPE "tx_indexer_status" AS ENUM ( + 'PROPOSED', + 'VERIFIED', + 'SIGNED' +); + +CREATE TYPE "tx_indexer_status_onchain" AS ENUM ( + 'PENDING', + 'SUCCESS', + 'FAIL' +); + +CREATE TABLE "plugin_policies" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "public_key" "text" NOT NULL, + "plugin_id" "plugin_id" NOT NULL, + "plugin_version" "text" NOT NULL, + "policy_version" integer NOT NULL, + "signature" "text" NOT NULL, + "recipe" "text" NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL +); + +CREATE TABLE "time_triggers" ( + "id" integer NOT NULL, + "policy_id" "uuid" NOT NULL, + "cron_expression" "text" NOT NULL, + "start_time" timestamp without time zone NOT NULL, + "end_time" timestamp without time zone, + "frequency" integer NOT NULL, + "interval" integer NOT NULL, + "last_execution" timestamp without time zone, + "status" "trigger_status" NOT NULL, + "created_at" timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + +CREATE SEQUENCE "time_triggers_id_seq" + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE "time_triggers_id_seq" OWNED BY "public"."time_triggers"."id"; + +CREATE TABLE "transaction_history" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "policy_id" "uuid" NOT NULL, + "tx_body" "text" NOT NULL, + "tx_hash" "text" NOT NULL, + "status" "transaction_status" NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"(), + "updated_at" timestamp with time zone DEFAULT "now"(), + "metadata" "jsonb", + "error_message" "text" +); + +CREATE TABLE "tx_indexer" ( + "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, + "plugin_id" character varying(255) NOT NULL, + "tx_hash" character varying(255), + "chain_id" integer NOT NULL, + "policy_id" "uuid" NOT NULL, + "from_public_key" character varying(255) NOT NULL, + "proposed_tx_hex" "text" NOT NULL, + "status" "tx_indexer_status" DEFAULT 'PROPOSED'::"public"."tx_indexer_status" NOT NULL, + "status_onchain" "tx_indexer_status_onchain", + "lost" boolean DEFAULT false NOT NULL, + "broadcasted_at" timestamp without time zone, + "created_at" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updated_at" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +ALTER TABLE ONLY "time_triggers" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."time_triggers_id_seq"'::"regclass"); + +ALTER TABLE ONLY "plugin_policies" + ADD CONSTRAINT "plugin_policies_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "time_triggers" + ADD CONSTRAINT "time_triggers_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "transaction_history" + ADD CONSTRAINT "transaction_history_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "tx_indexer" + ADD CONSTRAINT "tx_indexer_pkey" PRIMARY KEY ("id"); + +ALTER TABLE ONLY "transaction_history" + ADD CONSTRAINT "unique_tx_hash" UNIQUE ("tx_hash"); + +CREATE INDEX "idx_plugin_policies_active" ON "plugin_policies" USING "btree" ("active"); + +CREATE INDEX "idx_plugin_policies_plugin_id" ON "plugin_policies" USING "btree" ("plugin_id"); + +CREATE INDEX "idx_plugin_policies_public_key" ON "plugin_policies" USING "btree" ("public_key"); + +CREATE INDEX "idx_time_triggers_policy_id" ON "time_triggers" USING "btree" ("policy_id"); + +CREATE INDEX "idx_time_triggers_start_time" ON "time_triggers" USING "btree" ("start_time"); + +CREATE INDEX "idx_transaction_history_policy_id" ON "transaction_history" USING "btree" ("policy_id"); + +CREATE INDEX "idx_transaction_history_status" ON "transaction_history" USING "btree" ("status"); + +CREATE INDEX "idx_transaction_history_tx_hash" ON "transaction_history" USING "btree" ("tx_hash"); + +CREATE INDEX "idx_tx_indexer_status_onchain_lost" ON "tx_indexer" USING "btree" ("status_onchain", "lost"); + +ALTER TABLE ONLY "transaction_history" + ADD CONSTRAINT "fk_policy" FOREIGN KEY ("policy_id") REFERENCES "plugin_policies"("id"); + +ALTER TABLE ONLY "time_triggers" + ADD CONSTRAINT "time_triggers_policy_id_fkey" FOREIGN KEY ("policy_id") REFERENCES "plugin_policies"("id"); + +ALTER TABLE ONLY "transaction_history" + ADD CONSTRAINT "transaction_history_policy_id_fkey" FOREIGN KEY ("policy_id") REFERENCES "plugin_policies"("id"); +