diff --git a/apps/chain-indexer/drizzle/0008_good_galactus.sql b/apps/chain-indexer/drizzle/0008_good_galactus.sql new file mode 100644 index 0000000000..4680a79833 --- /dev/null +++ b/apps/chain-indexer/drizzle/0008_good_galactus.sql @@ -0,0 +1,58 @@ +CREATE TYPE "akash"."bme_mint_status" AS ENUM('mint_status_unspecified', 'mint_status_healthy', 'mint_status_warning', 'mint_status_halt_cr', 'mint_status_halt_oracle');--> statement-breakpoint +CREATE TABLE "akash"."bme_canceled_records" ( + "denom" text NOT NULL, + "to_denom" text NOT NULL, + "source" text NOT NULL, + "record_height" bigint NOT NULL, + "sequence" bigint NOT NULL, + "height" bigint NOT NULL, + "tx_index" integer, + "cancel_reason" text NOT NULL, + "owner_account_id" integer NOT NULL, + "to_account_id" integer NOT NULL, + "coins_to_burn_denom" text, + "coins_to_burn_amount" numeric(38, 0), + "denom_to_mint" text NOT NULL, + CONSTRAINT "bme_canceled_records_record_height_sequence_denom_to_denom_source_pk" PRIMARY KEY("record_height","sequence","denom","to_denom","source") +); +--> statement-breakpoint +CREATE TABLE "akash"."bme_ledger_records" ( + "denom" text NOT NULL, + "to_denom" text NOT NULL, + "source" text NOT NULL, + "record_height" bigint NOT NULL, + "sequence" bigint NOT NULL, + "height" bigint NOT NULL, + "tx_index" integer, + "burned_from_account_id" integer NOT NULL, + "minted_to_account_id" integer NOT NULL, + "burned_denom" text, + "burned_amount" numeric(38, 0) DEFAULT '0' NOT NULL, + "burned_price" numeric(38, 18), + "minted_denom" text, + "minted_amount" numeric(38, 0) DEFAULT '0' NOT NULL, + "minted_price" numeric(38, 18), + "spread_denom" text, + "spread_amount" numeric(38, 0), + "remint_credit_issued_amount" numeric(38, 0), + "remint_credit_accrued_amount" numeric(38, 0), + CONSTRAINT "bme_ledger_records_record_height_sequence_denom_to_denom_source_pk" PRIMARY KEY("record_height","sequence","denom","to_denom","source") +); +--> statement-breakpoint +CREATE TABLE "akash"."bme_status_changes" ( + "height" bigint NOT NULL, + "ordinal" integer NOT NULL, + "previous_status" "akash"."bme_mint_status" NOT NULL, + "new_status" "akash"."bme_mint_status" NOT NULL, + "collateral_ratio" numeric(38, 18) NOT NULL, + CONSTRAINT "bme_status_changes_height_ordinal_pk" PRIMARY KEY("height","ordinal") +); +--> statement-breakpoint +ALTER TABLE "akash"."bme_canceled_records" ADD CONSTRAINT "bme_canceled_records_owner_account_id_accounts_id_fk" FOREIGN KEY ("owner_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "akash"."bme_canceled_records" ADD CONSTRAINT "bme_canceled_records_to_account_id_accounts_id_fk" FOREIGN KEY ("to_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "akash"."bme_ledger_records" ADD CONSTRAINT "bme_ledger_records_burned_from_account_id_accounts_id_fk" FOREIGN KEY ("burned_from_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "akash"."bme_ledger_records" ADD CONSTRAINT "bme_ledger_records_minted_to_account_id_accounts_id_fk" FOREIGN KEY ("minted_to_account_id") REFERENCES "cosmos"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "bme_canceled_records_height_idx" ON "akash"."bme_canceled_records" USING btree ("height");--> statement-breakpoint +CREATE INDEX "bme_ledger_records_height_idx" ON "akash"."bme_ledger_records" USING btree ("height");--> statement-breakpoint +CREATE INDEX "bme_ledger_records_burned_denom_height_idx" ON "akash"."bme_ledger_records" USING btree ("burned_denom","height");--> statement-breakpoint +CREATE INDEX "bme_ledger_records_minted_denom_height_idx" ON "akash"."bme_ledger_records" USING btree ("minted_denom","height"); \ No newline at end of file diff --git a/apps/chain-indexer/drizzle/meta/0008_snapshot.json b/apps/chain-indexer/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000000..88fa96e1b6 --- /dev/null +++ b/apps/chain-indexer/drizzle/meta/0008_snapshot.json @@ -0,0 +1,2759 @@ +{ + "id": "2c31f1a9-1835-449a-899e-f6eaa1ebec5b", + "prevId": "a2860b14-74a0-48d3-825f-dc6cecbee0d3", + "version": "7", + "dialect": "postgresql", + "tables": { + "cosmos.account_balances": { + "name": "account_balances", + "schema": "cosmos", + "columns": { + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_balances_account_id_accounts_id_fk": { + "name": "account_balances_account_id_accounts_id_fk", + "tableFrom": "account_balances", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_balances_account_id_denom_pk": { + "name": "account_balances_account_id_denom_pk", + "columns": [ + "account_id", + "denom" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.account_txs": { + "name": "account_txs", + "schema": "cosmos", + "columns": { + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "account_tx_role", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_txs_account_id_accounts_id_fk": { + "name": "account_txs_account_id_accounts_id_fk", + "tableFrom": "account_txs", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_txs_account_id_height_tx_index_role_pk": { + "name": "account_txs_account_id_height_tx_index_role_pk", + "columns": [ + "account_id", + "height", + "tx_index", + "role" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.accounts": { + "name": "accounts", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_number": { + "name": "account_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_module_account": { + "name": "is_module_account", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "accounts_address_idx": { + "name": "accounts_address_idx", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.balance_changes": { + "name": "balance_changes", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delta": { + "name": "delta", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance_after": { + "name": "balance_after", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "balance_change_reason", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "event_index": { + "name": "event_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "counterparty_account_id": { + "name": "counterparty_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "balance_changes_account_denom_height_idx": { + "name": "balance_changes_account_denom_height_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "denom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "balance_changes_height_event_index_idx": { + "name": "balance_changes_height_event_index_idx", + "columns": [ + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "balance_changes_account_id_accounts_id_fk": { + "name": "balance_changes_account_id_accounts_id_fk", + "tableFrom": "balance_changes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "balance_changes_counterparty_account_id_accounts_id_fk": { + "name": "balance_changes_counterparty_account_id_accounts_id_fk", + "tableFrom": "balance_changes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "counterparty_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.bids": { + "name": "bids", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "oseq": { + "name": "oseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bseq": { + "name": "bseq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "bid_state", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bids_deployment_id_deployments_id_fk": { + "name": "bids_deployment_id_deployments_id_fk", + "tableFrom": "bids", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bids_provider_account_id_accounts_id_fk": { + "name": "bids_provider_account_id_accounts_id_fk", + "tableFrom": "bids", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "provider_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bids_deployment_id_gseq_oseq_bseq_provider_account_id_pk": { + "name": "bids_deployment_id_gseq_oseq_bseq_provider_account_id_pk", + "columns": [ + "deployment_id", + "gseq", + "oseq", + "bseq", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.blocks": { + "name": "blocks", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "datetime": { + "name": "datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "parent_hash": { + "name": "parent_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "proposer_address": { + "name": "proposer_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tx_count": { + "name": "tx_count", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.bme_canceled_records": { + "name": "bme_canceled_records", + "schema": "akash", + "columns": { + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_denom": { + "name": "to_denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_height": { + "name": "record_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cancel_reason": { + "name": "cancel_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "to_account_id": { + "name": "to_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "coins_to_burn_denom": { + "name": "coins_to_burn_denom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coins_to_burn_amount": { + "name": "coins_to_burn_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "denom_to_mint": { + "name": "denom_to_mint", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "bme_canceled_records_height_idx": { + "name": "bme_canceled_records_height_idx", + "columns": [ + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bme_canceled_records_owner_account_id_accounts_id_fk": { + "name": "bme_canceled_records_owner_account_id_accounts_id_fk", + "tableFrom": "bme_canceled_records", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bme_canceled_records_to_account_id_accounts_id_fk": { + "name": "bme_canceled_records_to_account_id_accounts_id_fk", + "tableFrom": "bme_canceled_records", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "to_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bme_canceled_records_record_height_sequence_denom_to_denom_source_pk": { + "name": "bme_canceled_records_record_height_sequence_denom_to_denom_source_pk", + "columns": [ + "record_height", + "sequence", + "denom", + "to_denom", + "source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.bme_ledger_records": { + "name": "bme_ledger_records", + "schema": "akash", + "columns": { + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_denom": { + "name": "to_denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record_height": { + "name": "record_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "burned_from_account_id": { + "name": "burned_from_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minted_to_account_id": { + "name": "minted_to_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "burned_denom": { + "name": "burned_denom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "burned_amount": { + "name": "burned_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "burned_price": { + "name": "burned_price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": false + }, + "minted_denom": { + "name": "minted_denom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "minted_amount": { + "name": "minted_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "minted_price": { + "name": "minted_price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": false + }, + "spread_denom": { + "name": "spread_denom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spread_amount": { + "name": "spread_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "remint_credit_issued_amount": { + "name": "remint_credit_issued_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "remint_credit_accrued_amount": { + "name": "remint_credit_accrued_amount", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "bme_ledger_records_height_idx": { + "name": "bme_ledger_records_height_idx", + "columns": [ + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bme_ledger_records_burned_denom_height_idx": { + "name": "bme_ledger_records_burned_denom_height_idx", + "columns": [ + { + "expression": "burned_denom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bme_ledger_records_minted_denom_height_idx": { + "name": "bme_ledger_records_minted_denom_height_idx", + "columns": [ + { + "expression": "minted_denom", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bme_ledger_records_burned_from_account_id_accounts_id_fk": { + "name": "bme_ledger_records_burned_from_account_id_accounts_id_fk", + "tableFrom": "bme_ledger_records", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "burned_from_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bme_ledger_records_minted_to_account_id_accounts_id_fk": { + "name": "bme_ledger_records_minted_to_account_id_accounts_id_fk", + "tableFrom": "bme_ledger_records", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "minted_to_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bme_ledger_records_record_height_sequence_denom_to_denom_source_pk": { + "name": "bme_ledger_records_record_height_sequence_denom_to_denom_source_pk", + "columns": [ + "record_height", + "sequence", + "denom", + "to_denom", + "source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.bme_status_changes": { + "name": "bme_status_changes", + "schema": "akash", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_status": { + "name": "previous_status", + "type": "bme_mint_status", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true + }, + "new_status": { + "name": "new_status", + "type": "bme_mint_status", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true + }, + "collateral_ratio": { + "name": "collateral_ratio", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bme_status_changes_height_ordinal_pk": { + "name": "bme_status_changes_height_ordinal_pk", + "columns": [ + "height", + "ordinal" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.delegations": { + "name": "delegations", + "schema": "cosmos", + "columns": { + "delegator_account_id": { + "name": "delegator_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validator_operator_address": { + "name": "validator_operator_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shares": { + "name": "shares", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "delegations_delegator_account_id_accounts_id_fk": { + "name": "delegations_delegator_account_id_accounts_id_fk", + "tableFrom": "delegations", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "delegator_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "delegations_delegator_account_id_validator_operator_address_pk": { + "name": "delegations_delegator_account_id_validator_operator_address_pk", + "columns": [ + "delegator_account_id", + "validator_operator_address" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_events": { + "name": "deployment_events", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "msg_index": { + "name": "msg_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "deployment_event_type", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_events_deployment_id_deployments_id_fk": { + "name": "deployment_events_deployment_id_deployments_id_fk", + "tableFrom": "deployment_events", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_events_deployment_id_height_ordinal_pk": { + "name": "deployment_events_deployment_id_height_ordinal_pk", + "columns": [ + "deployment_id", + "height", + "ordinal" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_group_resources": { + "name": "deployment_group_resources", + "schema": "akash", + "columns": { + "deployment_group_id": { + "name": "deployment_group_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idx": { + "name": "idx", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_vendor": { + "name": "gpu_vendor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gpu_model": { + "name": "gpu_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "price_denom": { + "name": "price_denom", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_group_resources_deployment_group_id_deployment_groups_id_fk": { + "name": "deployment_group_resources_deployment_group_id_deployment_groups_id_fk", + "tableFrom": "deployment_group_resources", + "tableTo": "deployment_groups", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deployment_group_resources_deployment_group_id_idx_pk": { + "name": "deployment_group_resources_deployment_group_id_idx_pk", + "columns": [ + "deployment_group_id", + "idx" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployment_groups": { + "name": "deployment_groups", + "schema": "akash", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "group_state", + "typeSchema": "akash", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "deployment_groups_deployment_gseq_idx": { + "name": "deployment_groups_deployment_gseq_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gseq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_groups_deployment_id_deployments_id_fk": { + "name": "deployment_groups_deployment_id_deployments_id_fk", + "tableFrom": "deployment_groups", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.deployments": { + "name": "deployments", + "schema": "akash", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dseq": { + "name": "dseq", + "type": "numeric(20, 0)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deposit": { + "name": "deposit", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "block_rate": { + "name": "block_rate", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_withdraw_height": { + "name": "last_withdraw_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_processed_height": { + "name": "last_processed_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "deployment_close_reason", + "typeSchema": "akash", + "primaryKey": false, + "notNull": false + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deployments_owner_dseq_idx": { + "name": "deployments_owner_dseq_idx", + "columns": [ + { + "expression": "owner_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dseq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployments_owner_created_idx": { + "name": "deployments_owner_created_idx", + "columns": [ + { + "expression": "owner_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "deployments_open_idx": { + "name": "deployments_open_idx", + "columns": [ + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"akash\".\"deployments\".\"closed_height\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_owner_account_id_accounts_id_fk": { + "name": "deployments_owner_account_id_accounts_id_fk", + "tableFrom": "deployments", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.indexer_state": { + "name": "indexer_state", + "schema": "", + "columns": { + "stream": { + "name": "stream", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_height": { + "name": "last_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.leases": { + "name": "leases", + "schema": "akash", + "columns": { + "deployment_id": { + "name": "deployment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deployment_group_id": { + "name": "deployment_group_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gseq": { + "name": "gseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "oseq": { + "name": "oseq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bseq": { + "name": "bseq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true + }, + "denom": { + "name": "denom", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "withdrawn_amount": { + "name": "withdrawn_amount", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "predicted_closed_height": { + "name": "predicted_closed_height", + "type": "numeric(30, 0)", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "closed_height": { + "name": "closed_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cpu_units": { + "name": "cpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gpu_units": { + "name": "gpu_units", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "memory_bytes": { + "name": "memory_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ephemeral_storage_bytes": { + "name": "ephemeral_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "persistent_storage_bytes": { + "name": "persistent_storage_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "leases_provider_idx": { + "name": "leases_provider_idx", + "columns": [ + { + "expression": "provider_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "closed_height", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_height", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leases_open_idx": { + "name": "leases_open_idx", + "columns": [ + { + "expression": "deployment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"akash\".\"leases\".\"closed_height\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leases_deployment_id_deployments_id_fk": { + "name": "leases_deployment_id_deployments_id_fk", + "tableFrom": "leases", + "tableTo": "deployments", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "leases_deployment_group_id_deployment_groups_id_fk": { + "name": "leases_deployment_group_id_deployment_groups_id_fk", + "tableFrom": "leases", + "tableTo": "deployment_groups", + "schemaTo": "akash", + "columnsFrom": [ + "deployment_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "leases_provider_account_id_accounts_id_fk": { + "name": "leases_provider_account_id_accounts_id_fk", + "tableFrom": "leases", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "provider_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "leases_deployment_id_gseq_oseq_bseq_provider_account_id_pk": { + "name": "leases_deployment_id_gseq_oseq_bseq_provider_account_id_pk", + "columns": [ + "deployment_id", + "gseq", + "oseq", + "bseq", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.message_dead_letters": { + "name": "message_dead_letters", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw": { + "name": "raw", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_dead_letters_type_id_idx": { + "name": "message_dead_letters_type_id_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_dead_letters_type_id_message_types_id_fk": { + "name": "message_dead_letters_type_id_message_types_id_fk", + "tableFrom": "message_dead_letters", + "tableTo": "message_types", + "schemaTo": "cosmos", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_dead_letters_height_tx_index_index_pk": { + "name": "message_dead_letters_height_tx_index_index_pk", + "columns": [ + "height", + "tx_index", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.message_types": { + "name": "message_types", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "message_types_type_idx": { + "name": "message_types_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.messages": { + "name": "messages", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tx_index": { + "name": "tx_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_type_id_idx": { + "name": "messages_type_id_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_type_id_message_types_id_fk": { + "name": "messages_type_id_message_types_id_fk", + "tableFrom": "messages", + "tableTo": "message_types", + "schemaTo": "cosmos", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "messages_height_tx_index_index_pk": { + "name": "messages_height_tx_index_index_pk", + "columns": [ + "height", + "tx_index", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposal_deposits": { + "name": "proposal_deposits", + "schema": "cosmos", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "depositor_account_id": { + "name": "depositor_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposal_deposits_depositor_account_id_accounts_id_fk": { + "name": "proposal_deposits_depositor_account_id_accounts_id_fk", + "tableFrom": "proposal_deposits", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "depositor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proposal_deposits_proposal_id_depositor_account_id_height_pk": { + "name": "proposal_deposits_proposal_id_depositor_account_id_height_pk", + "columns": [ + "proposal_id", + "depositor_account_id", + "height" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposal_votes": { + "name": "proposal_votes", + "schema": "cosmos", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "voter_account_id": { + "name": "voter_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposal_votes_voter_account_id_accounts_id_fk": { + "name": "proposal_votes_voter_account_id_accounts_id_fk", + "tableFrom": "proposal_votes", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "voter_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "proposal_votes_proposal_id_voter_account_id_pk": { + "name": "proposal_votes_proposal_id_voter_account_id_pk", + "columns": [ + "proposal_id", + "voter_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.proposals": { + "name": "proposals", + "schema": "cosmos", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "proposer_account_id": { + "name": "proposer_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "proposal_status", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": true + }, + "submit_time": { + "name": "submit_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deposit_end_time": { + "name": "deposit_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voting_start_time": { + "name": "voting_start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voting_end_time": { + "name": "voting_end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_deposit": { + "name": "total_deposit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_tally_yes": { + "name": "final_tally_yes", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_abstain": { + "name": "final_tally_abstain", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_no": { + "name": "final_tally_no", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "final_tally_no_with_veto": { + "name": "final_tally_no_with_veto", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "submit_height": { + "name": "submit_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "proposals_proposer_account_id_accounts_id_fk": { + "name": "proposals_proposer_account_id_accounts_id_fk", + "tableFrom": "proposals", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "proposer_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.provider_audit_signatures": { + "name": "provider_audit_signatures", + "schema": "akash", + "columns": { + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "auditor_account_id": { + "name": "auditor_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "provider_audit_signatures_owner_account_id_accounts_id_fk": { + "name": "provider_audit_signatures_owner_account_id_accounts_id_fk", + "tableFrom": "provider_audit_signatures", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "provider_audit_signatures_auditor_account_id_accounts_id_fk": { + "name": "provider_audit_signatures_auditor_account_id_accounts_id_fk", + "tableFrom": "provider_audit_signatures", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "auditor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "provider_audit_signatures_owner_account_id_auditor_account_id_key_pk": { + "name": "provider_audit_signatures_owner_account_id_auditor_account_id_key_pk", + "columns": [ + "owner_account_id", + "auditor_account_id", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "akash.providers": { + "name": "providers", + "schema": "akash", + "columns": { + "owner_account_id": { + "name": "owner_account_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "host_uri": { + "name": "host_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_processed_height": { + "name": "last_processed_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_height": { + "name": "created_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_height": { + "name": "updated_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "deleted_height": { + "name": "deleted_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "providers_owner_account_id_accounts_id_fk": { + "name": "providers_owner_account_id_accounts_id_fk", + "tableFrom": "providers", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.transactions": { + "name": "transactions", + "schema": "cosmos", + "columns": { + "height": { + "name": "height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "gas_used": { + "name": "gas_used", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "gas_wanted": { + "name": "gas_wanted", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "fee": { + "name": "fee", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "transactions_hash_idx": { + "name": "transactions_hash_idx", + "columns": [ + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "transactions_height_index_pk": { + "name": "transactions_height_index_pk", + "columns": [ + "height", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.unbonding_delegations": { + "name": "unbonding_delegations", + "schema": "cosmos", + "columns": { + "delegator_account_id": { + "name": "delegator_account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "validator_operator_address": { + "name": "validator_operator_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_height": { + "name": "creation_height", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "completion_time": { + "name": "completion_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "initial_balance": { + "name": "initial_balance", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "unbonding_delegations_delegator_account_id_accounts_id_fk": { + "name": "unbonding_delegations_delegator_account_id_accounts_id_fk", + "tableFrom": "unbonding_delegations", + "tableTo": "accounts", + "schemaTo": "cosmos", + "columnsFrom": [ + "delegator_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "unbonding_delegations_delegator_account_id_validator_operator_address_creation_height_pk": { + "name": "unbonding_delegations_delegator_account_id_validator_operator_address_creation_height_pk", + "columns": [ + "delegator_account_id", + "validator_operator_address", + "creation_height" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "cosmos.validators": { + "name": "validators", + "schema": "cosmos", + "columns": { + "operator_address": { + "name": "operator_address", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_address": { + "name": "account_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hex_address": { + "name": "hex_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moniker": { + "name": "moniker", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity": { + "name": "identity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "security_contact": { + "name": "security_contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commission_rate": { + "name": "commission_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "commission_max_rate": { + "name": "commission_max_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "commission_max_change_rate": { + "name": "commission_max_change_rate", + "type": "numeric(20, 18)", + "primaryKey": false, + "notNull": false + }, + "min_self_delegation": { + "name": "min_self_delegation", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "jailed": { + "name": "jailed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "validator_status", + "typeSchema": "cosmos", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "numeric(38, 0)", + "primaryKey": false, + "notNull": false + }, + "delegator_shares": { + "name": "delegator_shares", + "type": "numeric(38, 18)", + "primaryKey": false, + "notNull": false + }, + "unbonding_height": { + "name": "unbonding_height", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "unbonding_time": { + "name": "unbonding_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "cosmos.account_tx_role": { + "name": "account_tx_role", + "schema": "cosmos", + "values": [ + "signer", + "sender", + "receiver" + ] + }, + "cosmos.balance_change_reason": { + "name": "balance_change_reason", + "schema": "cosmos", + "values": [ + "genesis", + "transfer", + "fee", + "reward", + "commission", + "slash", + "gov", + "ibc", + "escrow", + "bme", + "mint", + "burn", + "staking" + ] + }, + "akash.bid_state": { + "name": "bid_state", + "schema": "akash", + "values": [ + "open", + "active", + "closed" + ] + }, + "akash.bme_mint_status": { + "name": "bme_mint_status", + "schema": "akash", + "values": [ + "mint_status_unspecified", + "mint_status_healthy", + "mint_status_warning", + "mint_status_halt_cr", + "mint_status_halt_oracle" + ] + }, + "akash.deployment_close_reason": { + "name": "deployment_close_reason", + "schema": "akash", + "values": [ + "close_message", + "overdrawn", + "close_event" + ] + }, + "akash.deployment_event_type": { + "name": "deployment_event_type", + "schema": "akash", + "values": [ + "created", + "deposited", + "updated", + "closed", + "group_closed", + "group_paused", + "group_started", + "bid_created", + "bid_closed", + "lease_created", + "lease_closed", + "lease_withdrawn" + ] + }, + "akash.group_state": { + "name": "group_state", + "schema": "akash", + "values": [ + "open", + "paused", + "closed" + ] + }, + "cosmos.proposal_status": { + "name": "proposal_status", + "schema": "cosmos", + "values": [ + "deposit_period", + "voting_period", + "passed", + "rejected", + "failed" + ] + }, + "cosmos.validator_status": { + "name": "validator_status", + "schema": "cosmos", + "values": [ + "unbonded", + "unbonding", + "bonded" + ] + }, + "cosmos.vote_option": { + "name": "vote_option", + "schema": "cosmos", + "values": [ + "yes", + "abstain", + "no", + "no_with_veto" + ] + } + }, + "schemas": { + "akash": "akash", + "cosmos": "cosmos" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/chain-indexer/drizzle/meta/_journal.json b/apps/chain-indexer/drizzle/meta/_journal.json index dc53cc0966..19bebd6d6b 100644 --- a/apps/chain-indexer/drizzle/meta/_journal.json +++ b/apps/chain-indexer/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1786898959325, "tag": "0007_secret_overlord", "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1786949585273, + "tag": "0008_good_galactus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/chain-indexer/src/akash/akash-deriver.ts b/apps/chain-indexer/src/akash/akash-deriver.ts index d69315550c..64b67088da 100644 --- a/apps/chain-indexer/src/akash/akash-deriver.ts +++ b/apps/chain-indexer/src/akash/akash-deriver.ts @@ -1,5 +1,5 @@ import type { AkashBlockChanges, AkashChange, AkashChangeBody } from "@src/akash/akash-changes"; -import { asInteger, asRecord, asString } from "@src/akash/json"; +import { asInteger, asRecord, asString, parseJsonRecord } from "@src/akash/json"; import { normalizeAuditMessage } from "@src/akash/normalize-audit"; import { normalizeDeploymentMessage } from "@src/akash/normalize-deployment"; import { normalizeMarketMessage } from "@src/akash/normalize-market"; @@ -116,14 +116,14 @@ function legacyLeaseClosed(attributes: Record): AkashChangeBody } function typedDeploymentClosed(attributes: Record): AkashChangeBody | null { - const id = parseIdAttribute(attributes.id); + const id = parseJsonRecord(attributes.id); const owner = asString(id?.owner); const dseq = asUint64String(id?.dseq); return owner && dseq ? { kind: "deploymentClosedEvent", key: { owner, dseq } } : null; } function typedLeaseClosed(attributes: Record): AkashChangeBody | null { - const id = parseIdAttribute(attributes.id); + const id = parseJsonRecord(attributes.id); const owner = asString(id?.owner); const dseq = asUint64String(id?.dseq); const gseq = asInteger(id?.gseq); @@ -134,15 +134,3 @@ function typedLeaseClosed(attributes: Record): AkashChangeBody | } return { kind: "leaseClosedEvent", key: { owner, dseq }, gseq, oseq, bseq: asInteger(id?.bseq), provider }; } - -/** The typed events carry their id as a JSON string attribute. */ -function parseIdAttribute(raw: string | undefined): Record | null { - if (!raw) { - return null; - } - try { - return asRecord(JSON.parse(raw)); - } catch { - return null; - } -} diff --git a/apps/chain-indexer/src/akash/akash-writer.service.ts b/apps/chain-indexer/src/akash/akash-writer.service.ts index 605f4e699b..9ff7faf6d7 100644 --- a/apps/chain-indexer/src/akash/akash-writer.service.ts +++ b/apps/chain-indexer/src/akash/akash-writer.service.ts @@ -11,6 +11,7 @@ import { sumLeaseRate } from "@src/akash/settlement"; import { insertChunked } from "@src/db/insert-chunked"; import { Accounts, Bids, DeploymentEvents, DeploymentGroupResources, DeploymentGroups, Deployments, Leases } from "@src/db/schema"; import { sqlExcluded } from "@src/db/sql-excluded"; +import { requireAccountId } from "@src/pipeline/balance/account-interner.service"; import type { ChainTransaction } from "@src/providers/db.provider"; import { LoggerService } from "@src/providers/logging.provider"; @@ -80,7 +81,7 @@ export class AkashWriter { } return [...byKey.values()] - .map(key => ({ key, ownerAccountId: this.#requireId(accountIds, key.owner) })) + .map(key => ({ key, ownerAccountId: requireAccountId(accountIds, key.owner) })) .sort((a, b) => a.ownerAccountId - b.ownerAccountId || compareDseq(a.key.dseq, b.key.dseq)); } @@ -234,7 +235,7 @@ export class AkashWriter { deploymentIds: Map ): Promise { const rows = touched.map(state => ({ - ownerAccountId: this.#requireId(accountIds, state.key.owner), + ownerAccountId: requireAccountId(accountIds, state.key.owner), dseq: state.key.dseq, denom: state.denom, deposit: state.deposit.toString(), @@ -278,7 +279,7 @@ export class AkashWriter { const idByOwnerDseq = new Map(inserted.map(row => [ownerDseqKey(row.ownerAccountId, row.dseq), row.id])); for (const state of touched) { - const id = idByOwnerDseq.get(ownerDseqKey(this.#requireId(accountIds, state.key.owner), state.key.dseq)); + const id = idByOwnerDseq.get(ownerDseqKey(requireAccountId(accountIds, state.key.owner), state.key.dseq)); if (id !== undefined) { deploymentIds.set(stateKey(state.key), id); } @@ -288,9 +289,9 @@ export class AkashWriter { if (missing.length > 0) { const rowsForMissing = await this.#selectDeploymentsForUpdate( tx, - missing.map(state => ({ key: state.key, ownerAccountId: this.#requireId(accountIds, state.key.owner) })) + missing.map(state => ({ key: state.key, ownerAccountId: requireAccountId(accountIds, state.key.owner) })) ); - const keyByOwnerDseq = new Map(missing.map(state => [ownerDseqKey(this.#requireId(accountIds, state.key.owner), state.key.dseq), state.key])); + const keyByOwnerDseq = new Map(missing.map(state => [ownerDseqKey(requireAccountId(accountIds, state.key.owner), state.key.dseq), state.key])); for (const row of rowsForMissing) { const key = keyByOwnerDseq.get(ownerDseqKey(row.ownerAccountId, row.dseq)); if (key) { @@ -360,7 +361,7 @@ export class AkashWriter { gseq: bid.gseq, oseq: bid.oseq, bseq: bid.bseq, - providerAccountId: this.#requireId(accountIds, bid.provider), + providerAccountId: requireAccountId(accountIds, bid.provider), price: decToString(bid.price), denom: bid.denom, state: bid.state, @@ -402,7 +403,7 @@ export class AkashWriter { gseq: lease.gseq, oseq: lease.oseq, bseq: lease.bseq, - providerAccountId: this.#requireId(accountIds, lease.provider), + providerAccountId: requireAccountId(accountIds, lease.provider), price: decToString(lease.price), denom: lease.denom, balance: decToString(lease.balance), @@ -467,14 +468,6 @@ export class AkashWriter { } } - #requireId(accountIds: Map, address: string): number { - const id = accountIds.get(address); - if (id === undefined) { - throw new Error(`No interned account id for address ${address}`); - } - return id; - } - #requireAddress(addressesById: Map, accountId: number): string { const address = addressesById.get(accountId); if (address === undefined) { diff --git a/apps/chain-indexer/src/akash/json.ts b/apps/chain-indexer/src/akash/json.ts index 360239ed32..9cf67ff8d4 100644 --- a/apps/chain-indexer/src/akash/json.ts +++ b/apps/chain-indexer/src/akash/json.ts @@ -6,6 +6,18 @@ export function asString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } +/** Typed proto events carry structured attributes (ids, coins) as JSON strings. */ +export function parseJsonRecord(raw: string | undefined): Record | null { + if (!raw) { + return null; + } + try { + return asRecord(JSON.parse(raw)); + } catch { + return null; + } +} + export function asInteger(value: unknown): number | null { if (typeof value === "number") { return Number.isSafeInteger(value) && value >= 0 ? value : null; diff --git a/apps/chain-indexer/src/akash/provider-writer.service.ts b/apps/chain-indexer/src/akash/provider-writer.service.ts index f38198f0e3..1848aabe74 100644 --- a/apps/chain-indexer/src/akash/provider-writer.service.ts +++ b/apps/chain-indexer/src/akash/provider-writer.service.ts @@ -7,6 +7,7 @@ import { isProviderAuditChange, isProviderChange, isProviderRegistryChange } fro import { INSERT_CHUNK_SIZE } from "@src/db/insert-chunk-size"; import { ProviderAuditSignatures, Providers } from "@src/db/schema"; import { sqlExcluded } from "@src/db/sql-excluded"; +import { requireAccountId } from "@src/pipeline/balance/account-interner.service"; import type { ChainTransaction } from "@src/providers/db.provider"; import { LoggerService } from "@src/providers/logging.provider"; @@ -125,7 +126,7 @@ export class ProviderWriter { if (isProviderAuditChange(change)) { continue; } - const ownerAccountId = this.#requireId(ownerIds, change.owner); + const ownerAccountId = requireAccountId(ownerIds, change.owner); if (skippedOwners.has(ownerAccountId)) { continue; } @@ -200,8 +201,8 @@ export class ProviderWriter { height: number, accountIds: Map ): Promise { - const ownerAccountId = this.#requireId(accountIds, change.owner); - const auditorAccountId = this.#requireId(accountIds, change.auditor); + const ownerAccountId = requireAccountId(accountIds, change.owner); + const auditorAccountId = requireAccountId(accountIds, change.auditor); const rows = dedupeByKeyLastWins(change.attributes).map(attribute => ({ ownerAccountId, auditorAccountId, @@ -230,8 +231,8 @@ export class ProviderWriter { accountIds: Map ): Promise { const identity = and( - eq(ProviderAuditSignatures.ownerAccountId, this.#requireId(accountIds, change.owner)), - eq(ProviderAuditSignatures.auditorAccountId, this.#requireId(accountIds, change.auditor)), + eq(ProviderAuditSignatures.ownerAccountId, requireAccountId(accountIds, change.owner)), + eq(ProviderAuditSignatures.auditorAccountId, requireAccountId(accountIds, change.auditor)), lte(ProviderAuditSignatures.height, height) ); await tx.delete(ProviderAuditSignatures).where(change.keys.length > 0 ? and(identity, inArray(ProviderAuditSignatures.key, change.keys)) : identity); @@ -242,7 +243,7 @@ export class ProviderWriter { for (const block of blocks) { for (const change of block.changes) { if (isProviderRegistryChange(change)) { - ownerIds.set(change.owner, this.#requireId(accountIds, change.owner)); + ownerIds.set(change.owner, requireAccountId(accountIds, change.owner)); } } } @@ -261,14 +262,6 @@ export class ProviderWriter { } this.#logger.warn({ event: "PROVIDER_ORPHAN_REFERENCE", count: warnings.length, samples: warnings.slice(0, 5) }); } - - #requireId(accountIds: Map, address: string): number { - const id = accountIds.get(address); - if (id === undefined) { - throw new Error(`No interned account id for address ${address}`); - } - return id; - } } /** diff --git a/apps/chain-indexer/src/bme/bme-deriver.spec.ts b/apps/chain-indexer/src/bme/bme-deriver.spec.ts new file mode 100644 index 0000000000..f6611ae2ff --- /dev/null +++ b/apps/chain-indexer/src/bme/bme-deriver.spec.ts @@ -0,0 +1,371 @@ +import { describe, expect, it } from "vitest"; + +import { collectBmeAddresses, deriveBmeChanges } from "@src/bme/bme-deriver"; +import type { DecodedBlock, DecodedEvent } from "@src/pipeline/decoded-block"; + +const BLOCK_TIME = new Date("2026-08-13T00:00:00Z"); + +const EXECUTED_EVENT_TYPE = "akash.bme.v1.EventLedgerRecordExecuted"; +const STATUS_CHANGE_EVENT_TYPE = "akash.bme.v1.EventMintStatusChange"; +const CANCELED_EVENT_TYPE = "akash.bme.v1.EventLedgerRecordCanceled"; + +describe("deriveBmeChanges", () => { + it("derives an executed mint record (uakt to uact) from a block event", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { + id: '{"denom":"uakt","to_denom":"uact","source":"bme","height":12000,"sequence":3}', + burned_from: '"akash1burner"', + minted_to: '"akash1minter"', + burned: '{"coin":{"denom":"uakt","amount":"1000000"},"price":"1.150000000000000000"}', + minted: '{"coin":{"denom":"uact","amount":"1150000"},"price":"1.000000000000000000"}', + spread: '{"denom":"uakt","amount":"25"}', + remint_credit_accrued: '{"coin":{"denom":"uakt","amount":"1000000"},"price":"1.150000000000000000"}' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + { + kind: "ledgerRecordExecuted", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 12000, sequence: 3 }, + burnedFrom: "akash1burner", + mintedTo: "akash1minter", + burned: { denom: "uakt", amount: "1000000", price: "1.150000000000000000" }, + minted: { denom: "uact", amount: "1150000", price: "1.000000000000000000" }, + spread: { denom: "uakt", amount: "25" }, + remintCreditIssued: null, + remintCreditAccrued: { denom: "uakt", amount: "1000000", price: "1.150000000000000000" }, + txIndex: null, + ordinal: 0 + } + ]); + }); + + it("treats JSON-null coin attributes as absent, matching the wire shape of a pure mint", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { + id: '{"denom":"uakt","to_denom":"uact","source":"akash1requester","height":"4844258","sequence":"1"}', + burned: "null", + burned_from: '"akash1requester"', + minted: '{"coin":{"denom":"uact","amount":"10529003"},"price":"1.000000000000000000"}', + minted_to: '"akash1requester"', + remint_credit_accrued: '{"coin":{"denom":"uakt","amount":"20000000"},"price":"0.526450156686029160"}', + remint_credit_issued: "null", + spread: '{"denom":"uact","amount":"26322"}' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + expect.objectContaining({ + kind: "ledgerRecordExecuted", + id: { denom: "uakt", toDenom: "uact", source: "akash1requester", recordHeight: 4844258, sequence: 1 }, + burned: null, + minted: { denom: "uact", amount: "10529003", price: "1.000000000000000000" }, + spread: { denom: "uact", amount: "26322" }, + remintCreditIssued: null, + remintCreditAccrued: { denom: "uakt", amount: "20000000", price: "0.526450156686029160" } + }) + ]); + }); + + it("derives an executed burn record (uact to uakt) with remint credit issued", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { + id: '{"denom":"uact","to_denom":"uakt","source":"bme","height":"12100","sequence":"4"}', + burned_from: '"akash1burner"', + minted_to: '"akash1minter"', + burned: '{"coin":{"denom":"uact","amount":"500000"},"price":"1.000000000000000000"}', + minted: '{"coin":{"denom":"uakt","amount":"434782"},"price":"1.150000000000000000"}', + remint_credit_issued: '{"coin":{"denom":"uakt","amount":"434782"},"price":"1.150000000000000000"}' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + expect.objectContaining({ + kind: "ledgerRecordExecuted", + id: { denom: "uact", toDenom: "uakt", source: "bme", recordHeight: 12100, sequence: 4 }, + burned: { denom: "uact", amount: "500000", price: "1.000000000000000000" }, + remintCreditIssued: { denom: "uakt", amount: "434782", price: "1.150000000000000000" }, + remintCreditAccrued: null, + spread: null + }) + ]); + }); + + it("passes IBC denoms and unquoted address attributes through raw", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { + id: '{"denom":"ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1","to_denom":"uact","source":"bme","height":9000,"sequence":1}', + burned_from: "akash1burner", + minted_to: "akash1minter", + burned: '{"coin":{"denom":"ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1","amount":"7"},"price":"1.000000000000000000"}' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + expect.objectContaining({ + id: expect.objectContaining({ denom: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1" }), + burnedFrom: "akash1burner", + mintedTo: "akash1minter", + burned: expect.objectContaining({ amount: "7" }), + minted: null + }) + ]); + }); + + it("derives a mint status change with JSON-quoted enum names and Dec ratio", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(STATUS_CHANGE_EVENT_TYPE, { + previous_status: '"mint_status_healthy"', + new_status: '"mint_status_warning"', + collateral_ratio: '"1.750000000000000000"' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + { + kind: "mintStatusChange", + previousStatus: "mint_status_healthy", + newStatus: "mint_status_warning", + collateralRatio: "1.750000000000000000", + txIndex: null, + ordinal: 0 + } + ]); + }); + + it("rejects a status change with a value outside the known mint statuses", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(STATUS_CHANGE_EVENT_TYPE, { + previous_status: '"mint_status_healthy"', + new_status: '"mint_status_brand_new"', + collateral_ratio: '"1.75"' + }) + ] + }) + ); + + expect(changes.changes).toEqual([]); + expect(changes.warnings).toHaveLength(1); + expect(changes.warnings[0]).toContain(STATUS_CHANGE_EVENT_TYPE); + }); + + it("derives a canceled record", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(CANCELED_EVENT_TYPE, { + id: '{"denom":"uakt","to_denom":"uact","source":"bme","height":12000,"sequence":5}', + cancel_reason: '"insufficient_funds"', + owner: '"akash1owner"', + to: '"akash1dest"', + coins_to_burn: '{"denom":"uakt","amount":"42"}', + denom_to_mint: '"uact"' + }) + ] + }) + ); + + expect(changes.warnings).toEqual([]); + expect(changes.changes).toEqual([ + { + kind: "ledgerRecordCanceled", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 12000, sequence: 5 }, + cancelReason: "insufficient_funds", + owner: "akash1owner", + to: "akash1dest", + coinsToBurn: { denom: "uakt", amount: "42" }, + denomToMint: "uact", + txIndex: null, + ordinal: 0 + } + ]); + }); + + it("assigns ordinals across transaction events then block events in scan order", () => { + const changes = deriveBmeChanges( + block({ + txEvents: [event(STATUS_CHANGE_EVENT_TYPE, statusChangeAttributes())], + blockEvents: [ + event(EXECUTED_EVENT_TYPE, executedAttributes({ sequence: 1 })), + event(STATUS_CHANGE_EVENT_TYPE, statusChangeAttributes()), + event(EXECUTED_EVENT_TYPE, executedAttributes({ sequence: 2 })) + ] + }) + ); + + expect(changes.changes.map(change => [change.kind, change.txIndex, change.ordinal])).toEqual([ + ["mintStatusChange", 0, 0], + ["ledgerRecordExecuted", null, 1], + ["mintStatusChange", null, 2], + ["ledgerRecordExecuted", null, 3] + ]); + }); + + it("skips events of failed transactions but keeps block events", () => { + const changes = deriveBmeChanges( + block({ + code: 1, + txEvents: [event(STATUS_CHANGE_EVENT_TYPE, statusChangeAttributes())], + blockEvents: [event(EXECUTED_EVENT_TYPE, executedAttributes({ sequence: 1 }))] + }) + ); + + expect(changes.changes.map(change => [change.kind, change.ordinal])).toEqual([["ledgerRecordExecuted", 0]]); + }); + + it("drops an event with a malformed id, keeps its ordinal slot and derives the rest", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { ...executedAttributes({ sequence: 1 }), id: "not-json" }), + event(EXECUTED_EVENT_TYPE, executedAttributes({ sequence: 2 })) + ] + }) + ); + + expect(changes.warnings).toHaveLength(1); + expect(changes.warnings[0]).toContain(EXECUTED_EVENT_TYPE); + expect(changes.changes.map(change => [change.kind, change.ordinal])).toEqual([["ledgerRecordExecuted", 1]]); + }); + + it("drops an executed event missing a party address", () => { + const attributes = executedAttributes({ sequence: 1 }); + delete (attributes as Record).minted_to; + + const changes = deriveBmeChanges(block({ blockEvents: [event(EXECUTED_EVENT_TYPE, attributes)] })); + + expect(changes.changes).toEqual([]); + expect(changes.warnings).toHaveLength(1); + }); + + it("drops an executed event whose coin amount is not an integer string", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event(EXECUTED_EVENT_TYPE, { + ...executedAttributes({ sequence: 1 }), + burned: '{"coin":{"denom":"uakt","amount":"1.5"},"price":"1.0"}' + }) + ] + }) + ); + + expect(changes.changes).toEqual([]); + expect(changes.warnings).toHaveLength(1); + }); + + it("ignores vault funded and unrelated events", () => { + const changes = deriveBmeChanges( + block({ + blockEvents: [ + event("akash.bme.v1.EventVaultFunded", { + amount: '{"denom":"uakt","amount":"100"}', + source: '"bme"', + new_vault_balance: '{"denom":"uakt","amount":"5000"}' + }), + event("transfer", { sender: "a", recipient: "b", amount: "1uakt" }) + ] + }) + ); + + expect(changes.changes).toEqual([]); + expect(changes.warnings).toEqual([]); + }); +}); + +describe("collectBmeAddresses", () => { + it("collects executed record parties and canceled record owner and destination", () => { + const executed = deriveBmeChanges(block({ blockEvents: [event(EXECUTED_EVENT_TYPE, executedAttributes({ sequence: 1 }))] })); + const canceled = deriveBmeChanges( + block({ + height: 101, + blockEvents: [ + event(CANCELED_EVENT_TYPE, { + id: '{"denom":"uakt","to_denom":"uact","source":"bme","height":12000,"sequence":5}', + cancel_reason: '"epsilon"', + owner: '"akash1owner"', + to: '"akash1dest"', + denom_to_mint: '"uact"' + }) + ] + }) + ); + + expect(collectBmeAddresses([executed, canceled])).toEqual(new Set(["akash1burner", "akash1minter", "akash1owner", "akash1dest"])); + }); +}); + +function executedAttributes(input: { sequence: number }): Record { + return { + id: `{"denom":"uakt","to_denom":"uact","source":"bme","height":12000,"sequence":${input.sequence}}`, + burned_from: '"akash1burner"', + minted_to: '"akash1minter"', + burned: '{"coin":{"denom":"uakt","amount":"1000000"},"price":"1.150000000000000000"}', + minted: '{"coin":{"denom":"uact","amount":"1150000"},"price":"1.000000000000000000"}' + }; +} + +function statusChangeAttributes(): Record { + return { + previous_status: '"mint_status_healthy"', + new_status: '"mint_status_warning"', + collateral_ratio: '"1.750000000000000000"' + }; +} + +function block(input: { height?: number; code?: number; txEvents?: DecodedEvent[]; blockEvents?: DecodedEvent[] }): DecodedBlock { + return { + height: input.height ?? 100, + datetime: BLOCK_TIME, + hash: Buffer.alloc(0), + parentHash: null, + proposerAddress: "P", + transactions: input.txEvents + ? [ + { + index: 0, + hash: Buffer.alloc(0), + code: input.code ?? 0, + gasUsed: 0, + gasWanted: 0, + fee: [], + messages: [], + events: input.txEvents, + signerAddresses: [] + } + ] + : [], + blockEvents: input.blockEvents ?? [] + }; +} + +function event(type: string, attributes: Record, msgIndex?: number): DecodedEvent { + return msgIndex === undefined ? { type, attributes } : { type, attributes, msgIndex }; +} diff --git a/apps/chain-indexer/src/bme/bme-deriver.ts b/apps/chain-indexer/src/bme/bme-deriver.ts new file mode 100644 index 0000000000..c8213d2263 --- /dev/null +++ b/apps/chain-indexer/src/bme/bme-deriver.ts @@ -0,0 +1,282 @@ +import { asInteger, asRecord, asString, parseJsonRecord } from "@src/akash/json"; +import { bmeMintStatus } from "@src/db/schema"; +import type { DecodedBlock, DecodedEvent } from "@src/pipeline/decoded-block"; + +const LEDGER_RECORD_EXECUTED_EVENT_TYPE = "akash.bme.v1.EventLedgerRecordExecuted"; +const MINT_STATUS_CHANGE_EVENT_TYPE = "akash.bme.v1.EventMintStatusChange"; +const LEDGER_RECORD_CANCELED_EVENT_TYPE = "akash.bme.v1.EventLedgerRecordCanceled"; + +const MINT_STATUSES = new Set(bmeMintStatus.enumValues); + +type BmeMintStatus = (typeof bmeMintStatus.enumValues)[number]; + +/** The on-chain LedgerRecordID; `recordHeight` is the record's creation height, not the execution block. */ +export interface BmeRecordId { + denom: string; + toDenom: string; + source: string; + recordHeight: number; + sequence: number; +} + +export interface BmeCoin { + denom: string; + amount: string; +} + +export interface BmeCoinPrice extends BmeCoin { + price: string | null; +} + +export type BmeChangeBody = + | { + kind: "ledgerRecordExecuted"; + id: BmeRecordId; + burnedFrom: string; + mintedTo: string; + burned: BmeCoinPrice | null; + minted: BmeCoinPrice | null; + spread: BmeCoin | null; + remintCreditIssued: BmeCoinPrice | null; + remintCreditAccrued: BmeCoinPrice | null; + } + | { kind: "mintStatusChange"; previousStatus: BmeMintStatus; newStatus: BmeMintStatus; collateralRatio: string } + | { + kind: "ledgerRecordCanceled"; + id: BmeRecordId; + cancelReason: string; + owner: string; + to: string; + coinsToBurn: BmeCoin | null; + denomToMint: string; + }; + +export type BmeChange = BmeChangeBody & { txIndex: number | null; ordinal: number }; + +export interface BmeBlockChanges { + height: number; + changes: BmeChange[]; + warnings: string[]; +} + +type ParsedChange = { change: BmeChangeBody } | { error: string }; + +/** + * Extracts the BME (burn-mint-equilibrium) lifecycle from a block's events: executed ledger records, + * mint status transitions and canceled records. Events of failed transactions are skipped; in practice + * BME fires in the EndBlocker, but tx events are scanned too since the natural keys dedupe either way. + * `ordinal` counts every BME-typed event in scan order — including ones that fail to parse — so a later + * parser fix replays with stable ordinals. Parse failures land in `warnings` for the writer to log; + * a malformed event must not halt the block. + */ +export function deriveBmeChanges(block: DecodedBlock): BmeBlockChanges { + const changes: BmeChange[] = []; + const warnings: string[] = []; + let ordinal = 0; + + const append = (events: DecodedEvent[], txIndex: number | null) => { + for (const event of events) { + const result = bmeChangeBody(event); + if (!result) { + continue; + } + if ("error" in result) { + warnings.push(`height=${block.height} ordinal=${ordinal} type=${event.type}: ${result.error}`); + } else { + changes.push({ ...result.change, txIndex, ordinal }); + } + ordinal += 1; + } + }; + + for (const tx of block.transactions) { + if (tx.code !== 0) { + continue; + } + append(tx.events, tx.index); + } + append(block.blockEvents, null); + + return { height: block.height, changes, warnings }; +} + +export function collectBmeAddresses(blocks: BmeBlockChanges[]): Set { + const addresses = new Set(); + for (const block of blocks) { + for (const change of block.changes) { + if (change.kind === "ledgerRecordExecuted") { + addresses.add(change.burnedFrom); + addresses.add(change.mintedTo); + } else if (change.kind === "ledgerRecordCanceled") { + addresses.add(change.owner); + addresses.add(change.to); + } + } + } + return addresses; +} + +function bmeChangeBody(event: DecodedEvent): ParsedChange | null { + switch (event.type) { + case LEDGER_RECORD_EXECUTED_EVENT_TYPE: + return ledgerRecordExecuted(event.attributes); + case MINT_STATUS_CHANGE_EVENT_TYPE: + return mintStatusChange(event.attributes); + case LEDGER_RECORD_CANCELED_EVENT_TYPE: + return ledgerRecordCanceled(event.attributes); + default: + return null; + } +} + +function ledgerRecordExecuted(attributes: Record): ParsedChange { + const id = parseRecordId(attributes.id); + if (!id) { + return { error: `unparseable id attribute: ${attributes.id}` }; + } + const burnedFrom = parseQuotedString(attributes.burned_from); + const mintedTo = parseQuotedString(attributes.minted_to); + if (!burnedFrom || !mintedTo) { + return { error: "missing burned_from or minted_to" }; + } + const burned = parseCoinPrice(attributes.burned); + const minted = parseCoinPrice(attributes.minted); + const spread = parseCoin(attributes.spread); + const remintCreditIssued = parseCoinPrice(attributes.remint_credit_issued); + const remintCreditAccrued = parseCoinPrice(attributes.remint_credit_accrued); + const malformed = [burned, minted, spread, remintCreditIssued, remintCreditAccrued].some(coin => coin === undefined); + if (malformed) { + return { error: "malformed coin attribute" }; + } + return { + change: { + kind: "ledgerRecordExecuted", + id, + burnedFrom, + mintedTo, + burned: burned ?? null, + minted: minted ?? null, + spread: spread ?? null, + remintCreditIssued: remintCreditIssued ?? null, + remintCreditAccrued: remintCreditAccrued ?? null + } + }; +} + +function mintStatusChange(attributes: Record): ParsedChange { + const previousStatus = parseQuotedString(attributes.previous_status); + const newStatus = parseQuotedString(attributes.new_status); + const collateralRatio = parseQuotedString(attributes.collateral_ratio); + if (!isMintStatus(previousStatus) || !isMintStatus(newStatus)) { + return { error: `unknown mint status: ${previousStatus} -> ${newStatus}` }; + } + if (!collateralRatio || !isDecString(collateralRatio)) { + return { error: `unparseable collateral_ratio: ${attributes.collateral_ratio}` }; + } + return { change: { kind: "mintStatusChange", previousStatus, newStatus, collateralRatio } }; +} + +function ledgerRecordCanceled(attributes: Record): ParsedChange { + const id = parseRecordId(attributes.id); + if (!id) { + return { error: `unparseable id attribute: ${attributes.id}` }; + } + const cancelReason = parseQuotedString(attributes.cancel_reason); + const owner = parseQuotedString(attributes.owner); + const to = parseQuotedString(attributes.to); + const denomToMint = parseQuotedString(attributes.denom_to_mint); + if (!cancelReason || !owner || !to || !denomToMint) { + return { error: "missing cancel_reason, owner, to or denom_to_mint" }; + } + const coinsToBurn = parseCoin(attributes.coins_to_burn); + if (coinsToBurn === undefined) { + return { error: "malformed coins_to_burn attribute" }; + } + return { change: { kind: "ledgerRecordCanceled", id, cancelReason, owner, to, coinsToBurn: coinsToBurn ?? null, denomToMint } }; +} + +function parseRecordId(raw: string | undefined): BmeRecordId | null { + const record = parseJsonRecord(raw); + const denom = asString(record?.denom); + const toDenom = asString(record?.to_denom); + const source = asString(record?.source); + const recordHeight = asInteger(record?.height); + const sequence = asInteger(record?.sequence); + if (!denom || !toDenom || !source || recordHeight === null || sequence === null) { + return null; + } + return { denom, toDenom, source, recordHeight, sequence }; +} + +/** + * `undefined` = the attribute is present but malformed; `null` = absent, which is a valid state. + * An unset proto message field reaches the wire as the literal JSON `null` (e.g. `burned` on a pure + * mint), so that counts as absent too. + */ +function parseCoinPrice(raw: string | undefined): BmeCoinPrice | null | undefined { + const parsed = parseJsonAttribute(raw); + if (parsed === null) { + return null; + } + const record = asRecord(parsed); + const coin = coinOf(asRecord(record?.coin)); + if (!coin) { + return undefined; + } + return { ...coin, price: asString(record?.price) }; +} + +function parseCoin(raw: string | undefined): BmeCoin | null | undefined { + const parsed = parseJsonAttribute(raw); + if (parsed === null) { + return null; + } + return coinOf(asRecord(parsed)) ?? undefined; +} + +/** `null` = absent (missing attribute or JSON null); `undefined` = present but not valid JSON. */ +function parseJsonAttribute(raw: string | undefined): unknown { + if (raw === undefined) { + return null; + } + try { + return JSON.parse(raw) ?? null; + } catch { + return undefined; + } +} + +function coinOf(record: Record | null): BmeCoin | null { + const denom = asString(record?.denom); + const amount = asString(record?.amount); + if (!denom || !amount || !/^\d+$/.test(amount)) { + return null; + } + return { denom, amount }; +} + +/** + * Strips the JSON string encoding CometBFT ABCI 2.0+ applies to proto string/Dec/enum attributes + * (e.g. `"bme"` or `"1.75"`), falling back to the raw value for unquoted legacy attributes. + */ +function parseQuotedString(raw: string | undefined): string | null { + if (!raw) { + return null; + } + if (raw.startsWith('"') && raw.endsWith('"')) { + try { + return asString(JSON.parse(raw)); + } catch { + return raw; + } + } + return raw; +} + +function isMintStatus(value: string | null): value is BmeMintStatus { + return value !== null && MINT_STATUSES.has(value); +} + +function isDecString(value: string): boolean { + return /^\d+(\.\d+)?$/.test(value); +} diff --git a/apps/chain-indexer/src/bme/bme-writer.service.spec.ts b/apps/chain-indexer/src/bme/bme-writer.service.spec.ts new file mode 100644 index 0000000000..b00651c3c7 --- /dev/null +++ b/apps/chain-indexer/src/bme/bme-writer.service.spec.ts @@ -0,0 +1,241 @@ +import { describe, expect, it } from "vitest"; +import { mock } from "vitest-mock-extended"; + +import type { BmeBlockChanges, BmeChange } from "@src/bme/bme-deriver"; +import { BmeWriter } from "@src/bme/bme-writer.service"; +import { BmeCanceledRecords, BmeLedgerRecords, BmeStatusChanges } from "@src/db/schema"; +import type { LoggerService } from "@src/providers/logging.provider"; + +import { buildTxFake, rowsFor } from "@test/fakes/build-tx-fake"; + +const BURNER = "akash1burner"; +const MINTER = "akash1minter"; +const OWNER = "akash1owner"; +const DEST = "akash1dest"; +const ACCOUNT_IDS = new Map([ + [BURNER, 3], + [MINTER, 4], + [OWNER, 5], + [DEST, 6] +]); + +describe(BmeWriter.name, () => { + it("writes executed records with interned account ids and flattened coin prices", async () => { + const { writer, tx, inserts } = setup(); + + await writer.write( + tx, + [ + blockChanges(100, [ + { + kind: "ledgerRecordExecuted", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 90, sequence: 3 }, + burnedFrom: BURNER, + mintedTo: MINTER, + burned: { denom: "uakt", amount: "1000000", price: "1.150000000000000000" }, + minted: { denom: "uact", amount: "1150000", price: "1.000000000000000000" }, + spread: { denom: "uakt", amount: "25" }, + remintCreditIssued: null, + remintCreditAccrued: { denom: "uakt", amount: "1000000", price: "1.150000000000000000" }, + txIndex: null, + ordinal: 0 + } + ]) + ], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, BmeLedgerRecords)).toEqual([ + { + denom: "uakt", + toDenom: "uact", + source: "bme", + recordHeight: 90, + sequence: 3, + height: 100, + txIndex: null, + burnedFromAccountId: 3, + mintedToAccountId: 4, + burnedDenom: "uakt", + burnedAmount: "1000000", + burnedPrice: "1.150000000000000000", + mintedDenom: "uact", + mintedAmount: "1150000", + mintedPrice: "1.000000000000000000", + spreadDenom: "uakt", + spreadAmount: "25", + remintCreditIssuedAmount: null, + remintCreditAccruedAmount: "1000000" + } + ]); + }); + + it("defaults absent coin sides to zero amounts and null denoms", async () => { + const { writer, tx, inserts } = setup(); + + await writer.write( + tx, + [ + blockChanges(100, [ + { + kind: "ledgerRecordExecuted", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 100, sequence: 1 }, + burnedFrom: BURNER, + mintedTo: MINTER, + burned: null, + minted: null, + spread: null, + remintCreditIssued: null, + remintCreditAccrued: null, + txIndex: 2, + ordinal: 0 + } + ]) + ], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, BmeLedgerRecords)).toEqual([ + expect.objectContaining({ + txIndex: 2, + burnedDenom: null, + burnedAmount: "0", + burnedPrice: null, + mintedDenom: null, + mintedAmount: "0", + mintedPrice: null, + spreadDenom: null, + spreadAmount: null + }) + ]); + }); + + it("writes status changes and canceled records to their tables", async () => { + const { writer, tx, inserts } = setup(); + + await writer.write( + tx, + [ + blockChanges(200, [ + { + kind: "mintStatusChange", + previousStatus: "mint_status_healthy", + newStatus: "mint_status_halt_oracle", + collateralRatio: "0.900000000000000000", + txIndex: null, + ordinal: 0 + }, + { + kind: "ledgerRecordCanceled", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 195, sequence: 7 }, + cancelReason: "insufficient_funds", + owner: OWNER, + to: DEST, + coinsToBurn: { denom: "uakt", amount: "42" }, + denomToMint: "uact", + txIndex: null, + ordinal: 1 + } + ]) + ], + ACCOUNT_IDS + ); + + expect(rowsFor(inserts, BmeStatusChanges)).toEqual([ + { + height: 200, + ordinal: 0, + previousStatus: "mint_status_healthy", + newStatus: "mint_status_halt_oracle", + collateralRatio: "0.900000000000000000" + } + ]); + expect(rowsFor(inserts, BmeCanceledRecords)).toEqual([ + { + denom: "uakt", + toDenom: "uact", + source: "bme", + recordHeight: 195, + sequence: 7, + height: 200, + txIndex: null, + cancelReason: "insufficient_funds", + ownerAccountId: 5, + toAccountId: 6, + coinsToBurnDenom: "uakt", + coinsToBurnAmount: "42", + denomToMint: "uact" + } + ]); + }); + + it("does nothing for blocks without changes or warnings", async () => { + const { writer, tx, inserts, logger } = setup(); + + await writer.write(tx, [blockChanges(100, [])], ACCOUNT_IDS); + + expect(inserts).toEqual([]); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it("throws when a change references an address that was not interned", async () => { + const { writer, tx } = setup(); + + const write = writer.write( + tx, + [ + blockChanges(100, [ + { + kind: "ledgerRecordExecuted", + id: { denom: "uakt", toDenom: "uact", source: "bme", recordHeight: 100, sequence: 1 }, + burnedFrom: "akash1unknown", + mintedTo: MINTER, + burned: null, + minted: null, + spread: null, + remintCreditIssued: null, + remintCreditAccrued: null, + txIndex: null, + ordinal: 0 + } + ]) + ], + ACCOUNT_IDS + ); + + await expect(write).rejects.toThrow("akash1unknown"); + }); + + it("logs deriver warnings once with count and samples", async () => { + const { writer, tx, logger } = setup(); + + await writer.write( + tx, + [ + { height: 100, changes: [], warnings: ["height=100 ordinal=0 type=akash.bme.v1.EventLedgerRecordExecuted: unparseable id attribute: x"] }, + { height: 101, changes: [], warnings: ["height=101 ordinal=0 type=akash.bme.v1.EventMintStatusChange: unknown mint status: a -> b"] } + ], + ACCOUNT_IDS + ); + + expect(logger.warn).toHaveBeenCalledExactlyOnceWith({ + event: "BME_EVENT_PARSE_FAILED", + count: 2, + samples: [ + "height=100 ordinal=0 type=akash.bme.v1.EventLedgerRecordExecuted: unparseable id attribute: x", + "height=101 ordinal=0 type=akash.bme.v1.EventMintStatusChange: unknown mint status: a -> b" + ] + }); + }); + + function blockChanges(height: number, changes: BmeChange[]): BmeBlockChanges { + return { height, changes, warnings: [] }; + } + + function setup() { + const { tx, inserts } = buildTxFake(); + const logger = mock(); + const writer = new BmeWriter(logger); + return { writer, tx, inserts, logger }; + } +}); diff --git a/apps/chain-indexer/src/bme/bme-writer.service.ts b/apps/chain-indexer/src/bme/bme-writer.service.ts new file mode 100644 index 0000000000..83766fb339 --- /dev/null +++ b/apps/chain-indexer/src/bme/bme-writer.service.ts @@ -0,0 +1,111 @@ +import { inject, singleton } from "tsyringe"; + +import type { BmeBlockChanges, BmeChange } from "@src/bme/bme-deriver"; +import { insertChunked } from "@src/db/insert-chunked"; +import { BmeCanceledRecords, BmeLedgerRecords, BmeStatusChanges } from "@src/db/schema"; +import { requireAccountId } from "@src/pipeline/balance/account-interner.service"; +import type { ChainTransaction } from "@src/providers/db.provider"; +import { LoggerService } from "@src/providers/logging.provider"; + +type LedgerRecordRow = typeof BmeLedgerRecords.$inferInsert; +type StatusChangeRow = typeof BmeStatusChanges.$inferInsert; +type CanceledRecordRow = typeof BmeCanceledRecords.$inferInsert; + +/** + * Persists the BME lifecycle inside the block transaction. Rows are plain conflict-ignoring appends — + * ledger and canceled records are keyed by the on-chain LedgerRecordID and status changes by + * (height, ordinal), so replaying a block is a no-op without locks or watermarks. + */ +@singleton() +export class BmeWriter { + readonly #logger: LoggerService; + + constructor(@inject(LoggerService) logger: LoggerService) { + this.#logger = logger; + this.#logger.setContext("BME_WRITER"); + } + + async write(tx: ChainTransaction, blocks: BmeBlockChanges[], accountIds: Map): Promise { + this.#logWarnings(blocks); + + const ledgerRecords: LedgerRecordRow[] = []; + const statusChanges: StatusChangeRow[] = []; + const canceledRecords: CanceledRecordRow[] = []; + + for (const block of blocks) { + for (const change of block.changes) { + if (change.kind === "ledgerRecordExecuted") { + ledgerRecords.push(this.#ledgerRecordRow(block.height, change, accountIds)); + } else if (change.kind === "mintStatusChange") { + statusChanges.push(this.#statusChangeRow(block.height, change)); + } else { + canceledRecords.push(this.#canceledRecordRow(block.height, change, accountIds)); + } + } + } + + await insertChunked(tx, BmeLedgerRecords, ledgerRecords); + await insertChunked(tx, BmeStatusChanges, statusChanges); + await insertChunked(tx, BmeCanceledRecords, canceledRecords); + } + + #ledgerRecordRow(height: number, change: Extract, accountIds: Map): LedgerRecordRow { + return { + denom: change.id.denom, + toDenom: change.id.toDenom, + source: change.id.source, + recordHeight: change.id.recordHeight, + sequence: change.id.sequence, + height, + txIndex: change.txIndex, + burnedFromAccountId: requireAccountId(accountIds, change.burnedFrom), + mintedToAccountId: requireAccountId(accountIds, change.mintedTo), + burnedDenom: change.burned?.denom ?? null, + burnedAmount: change.burned?.amount ?? "0", + burnedPrice: change.burned?.price ?? null, + mintedDenom: change.minted?.denom ?? null, + mintedAmount: change.minted?.amount ?? "0", + mintedPrice: change.minted?.price ?? null, + spreadDenom: change.spread?.denom ?? null, + spreadAmount: change.spread?.amount ?? null, + remintCreditIssuedAmount: change.remintCreditIssued?.amount ?? null, + remintCreditAccruedAmount: change.remintCreditAccrued?.amount ?? null + }; + } + + #statusChangeRow(height: number, change: Extract): StatusChangeRow { + return { + height, + ordinal: change.ordinal, + previousStatus: change.previousStatus, + newStatus: change.newStatus, + collateralRatio: change.collateralRatio + }; + } + + #canceledRecordRow(height: number, change: Extract, accountIds: Map): CanceledRecordRow { + return { + denom: change.id.denom, + toDenom: change.id.toDenom, + source: change.id.source, + recordHeight: change.id.recordHeight, + sequence: change.id.sequence, + height, + txIndex: change.txIndex, + cancelReason: change.cancelReason, + ownerAccountId: requireAccountId(accountIds, change.owner), + toAccountId: requireAccountId(accountIds, change.to), + coinsToBurnDenom: change.coinsToBurn?.denom ?? null, + coinsToBurnAmount: change.coinsToBurn?.amount ?? null, + denomToMint: change.denomToMint + }; + } + + #logWarnings(blocks: BmeBlockChanges[]): void { + const warnings = blocks.flatMap(block => block.warnings); + if (warnings.length === 0) { + return; + } + this.#logger.warn({ event: "BME_EVENT_PARSE_FAILED", count: warnings.length, samples: warnings.slice(0, 5) }); + } +} diff --git a/apps/chain-indexer/src/db/schema.ts b/apps/chain-indexer/src/db/schema.ts index 99fcb2dd0b..815cf23e22 100644 --- a/apps/chain-indexer/src/db/schema.ts +++ b/apps/chain-indexer/src/db/schema.ts @@ -551,3 +551,97 @@ export const ProviderAuditSignatures = akashSchema.table( }, t => [primaryKey({ columns: [t.ownerAccountId, t.auditorAccountId, t.key] })] ); + +export const bmeMintStatus = akashSchema.enum("bme_mint_status", [ + "mint_status_unspecified", + "mint_status_healthy", + "mint_status_warning", + "mint_status_halt_cr", + "mint_status_halt_oracle" +]); + +/** + * One row per executed BME burn/mint, keyed by the full on-chain LedgerRecordID. `record_height` is + * the record's creation height from the id and can precede `height`, the block whose EndBlocker + * executed it (pending records execute later). Amounts are the exact u-denom integers from the + * event; prices keep the chain's 18-decimal Dec precision. Denoms are stored raw (uakt/uact/ibc/...). + */ +export const BmeLedgerRecords = akashSchema.table( + "bme_ledger_records", + { + denom: text("denom").notNull(), + toDenom: text("to_denom").notNull(), + source: text("source").notNull(), + recordHeight: bigint("record_height", { mode: "number" }).notNull(), + sequence: bigint("sequence", { mode: "number" }).notNull(), + height: bigint("height", { mode: "number" }).notNull(), + txIndex: integer("tx_index"), + burnedFromAccountId: integer("burned_from_account_id") + .notNull() + .references(() => Accounts.id), + mintedToAccountId: integer("minted_to_account_id") + .notNull() + .references(() => Accounts.id), + burnedDenom: text("burned_denom"), + burnedAmount: numeric("burned_amount", { precision: 38, scale: 0 }).notNull().default("0"), + burnedPrice: numeric("burned_price", { precision: 38, scale: 18 }), + mintedDenom: text("minted_denom"), + mintedAmount: numeric("minted_amount", { precision: 38, scale: 0 }).notNull().default("0"), + mintedPrice: numeric("minted_price", { precision: 38, scale: 18 }), + spreadDenom: text("spread_denom"), + spreadAmount: numeric("spread_amount", { precision: 38, scale: 0 }), + remintCreditIssuedAmount: numeric("remint_credit_issued_amount", { precision: 38, scale: 0 }), + remintCreditAccruedAmount: numeric("remint_credit_accrued_amount", { precision: 38, scale: 0 }) + }, + t => [ + primaryKey({ columns: [t.recordHeight, t.sequence, t.denom, t.toDenom, t.source] }), + index("bme_ledger_records_height_idx").on(t.height), + index("bme_ledger_records_burned_denom_height_idx").on(t.burnedDenom, t.height), + index("bme_ledger_records_minted_denom_height_idx").on(t.mintedDenom, t.height) + ] +); + +/** + * BME circuit-breaker transitions. `ordinal` is the event's deterministic position among the block's + * BME events, so re-committing a block conflicts instead of duplicating. + */ +export const BmeStatusChanges = akashSchema.table( + "bme_status_changes", + { + height: bigint("height", { mode: "number" }).notNull(), + ordinal: integer("ordinal").notNull(), + previousStatus: bmeMintStatus("previous_status").notNull(), + newStatus: bmeMintStatus("new_status").notNull(), + collateralRatio: numeric("collateral_ratio", { precision: 38, scale: 18 }).notNull() + }, + t => [primaryKey({ columns: [t.height, t.ordinal] })] +); + +/** + * Canceled BME ledger records, parsed from EventLedgerRecordCanceled — the legacy indexer kept these + * only as raw staging rows. Same natural key as executed records; a canceled record has no supply + * impact, so no amounts land in `bme_ledger_records`. + */ +export const BmeCanceledRecords = akashSchema.table( + "bme_canceled_records", + { + denom: text("denom").notNull(), + toDenom: text("to_denom").notNull(), + source: text("source").notNull(), + recordHeight: bigint("record_height", { mode: "number" }).notNull(), + sequence: bigint("sequence", { mode: "number" }).notNull(), + height: bigint("height", { mode: "number" }).notNull(), + txIndex: integer("tx_index"), + cancelReason: text("cancel_reason").notNull(), + ownerAccountId: integer("owner_account_id") + .notNull() + .references(() => Accounts.id), + toAccountId: integer("to_account_id") + .notNull() + .references(() => Accounts.id), + coinsToBurnDenom: text("coins_to_burn_denom"), + coinsToBurnAmount: numeric("coins_to_burn_amount", { precision: 38, scale: 0 }), + denomToMint: text("denom_to_mint").notNull() + }, + t => [primaryKey({ columns: [t.recordHeight, t.sequence, t.denom, t.toDenom, t.source] }), index("bme_canceled_records_height_idx").on(t.height)] +); diff --git a/apps/chain-indexer/src/pipeline/balance/account-interner.service.ts b/apps/chain-indexer/src/pipeline/balance/account-interner.service.ts index c252d7c5c4..a4554e27e0 100644 --- a/apps/chain-indexer/src/pipeline/balance/account-interner.service.ts +++ b/apps/chain-indexer/src/pipeline/balance/account-interner.service.ts @@ -15,6 +15,15 @@ import { CHAIN_DB } from "@src/providers/db.provider"; * It runs on the base connection, not the commit transaction, so the interned rows are visible when the * transaction inserts ledger rows that reference them. */ +/** A miss means the caller's address-collection pass and its row-building pass disagree — an indexer bug, not chain data. */ +export function requireAccountId(accountIds: Map, address: string): number { + const id = accountIds.get(address); + if (id === undefined) { + throw new Error(`No interned account id for address ${address}`); + } + return id; +} + @singleton() export class AccountInterner { readonly #db: ChainDatabase; diff --git a/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts b/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts index eb276ac896..d06b7409dd 100644 --- a/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts +++ b/apps/chain-indexer/src/pipeline/block-committer.service.spec.ts @@ -5,6 +5,7 @@ import { mock } from "vitest-mock-extended"; import type { AkashWriter } from "@src/akash/akash-writer.service"; import type { ProviderWriter } from "@src/akash/provider-writer.service"; +import type { BmeWriter } from "@src/bme/bme-writer.service"; import { AccountTxs, Blocks, IndexerState, MessageDeadLetters, Messages, MessageTypes } from "@src/db/schema"; import type { GovWriter } from "@src/gov/gov-writer.service"; import type { AccountInterner } from "@src/pipeline/balance/account-interner.service"; @@ -268,6 +269,35 @@ describe(BlockCommitterService.name, () => { expect(providerWriter.write).toHaveBeenCalledWith(expect.anything(), akashWriter.write.mock.calls[0][1], akashWriter.write.mock.calls[0][2]); }); + + it("hands the bme writer the derived changes and interns the record parties", async () => { + const { committer, bmeWriter } = setup({ selectResults: [[{ id: 7, type: MSG_SEND }]] }); + + await committer.commit( + buildBlock([MSG_SEND], 10, { + events: [ + { + type: "akash.bme.v1.EventLedgerRecordExecuted", + attributes: { + id: '{"denom":"uakt","to_denom":"uact","source":"bme","height":9,"sequence":1}', + burned_from: '"akash1bmeburner"', + minted_to: '"akash1bmeminter"', + minted: '{"coin":{"denom":"uact","amount":"100"},"price":"1.000000000000000000"}' + } + } + ] + }) + ); + + expect(bmeWriter.write).toHaveBeenCalledWith( + expect.anything(), + [expect.objectContaining({ height: 10, changes: [expect.objectContaining({ kind: "ledgerRecordExecuted" })] })], + expect.any(Map) + ); + const accountIds = bmeWriter.write.mock.calls[0][2]; + expect(accountIds.get("akash1bmeburner")).toBeDefined(); + expect(accountIds.get("akash1bmeminter")).toBeDefined(); + }); }); function setup(input?: { @@ -321,9 +351,19 @@ describe(BlockCommitterService.name, () => { const govWriter = mock(); const akashWriter = mock(); const providerWriter = mock(); + const bmeWriter = mock(); const logger = mock(); - const committer = new BlockCommitterService(dbFake as unknown as ChainDatabase, interner, balanceWriter, govWriter, akashWriter, providerWriter, logger); - return { committer, insertedRows, conflictUpdates, deletions, interner, balanceWriter, govWriter, akashWriter, providerWriter, logger }; + const committer = new BlockCommitterService( + dbFake as unknown as ChainDatabase, + interner, + balanceWriter, + govWriter, + akashWriter, + providerWriter, + bmeWriter, + logger + ); + return { committer, insertedRows, conflictUpdates, deletions, interner, balanceWriter, govWriter, akashWriter, providerWriter, bmeWriter, logger }; } function buildBlock( diff --git a/apps/chain-indexer/src/pipeline/block-committer.service.ts b/apps/chain-indexer/src/pipeline/block-committer.service.ts index a1aec9c2b9..8d594c2e8e 100644 --- a/apps/chain-indexer/src/pipeline/block-committer.service.ts +++ b/apps/chain-indexer/src/pipeline/block-committer.service.ts @@ -7,12 +7,15 @@ import { collectAkashAddresses } from "@src/akash/akash-changes"; import { deriveAkashChanges } from "@src/akash/akash-deriver"; import { AkashWriter } from "@src/akash/akash-writer.service"; import { ProviderWriter } from "@src/akash/provider-writer.service"; +import type { BmeBlockChanges } from "@src/bme/bme-deriver"; +import { collectBmeAddresses, deriveBmeChanges } from "@src/bme/bme-deriver"; +import { BmeWriter } from "@src/bme/bme-writer.service"; import { INSERT_CHUNK_SIZE } from "@src/db/insert-chunk-size"; import { insertChunked } from "@src/db/insert-chunked"; import { AccountTxs, Blocks, IndexerState, MessageDeadLetters, Messages, MessageTypes, Transactions } from "@src/db/schema"; import { sqlExcluded } from "@src/db/sql-excluded"; import { GovWriter } from "@src/gov/gov-writer.service"; -import { AccountInterner } from "@src/pipeline/balance/account-interner.service"; +import { AccountInterner, requireAccountId } from "@src/pipeline/balance/account-interner.service"; import type { DerivedAccountTx } from "@src/pipeline/balance/account-tx-deriver"; import { deriveAccountTxs } from "@src/pipeline/balance/account-tx-deriver"; import type { DerivedBalanceChange } from "@src/pipeline/balance/balance-deriver"; @@ -49,6 +52,7 @@ export class BlockCommitterService { readonly #govWriter: GovWriter; readonly #akashWriter: AkashWriter; readonly #providerWriter: ProviderWriter; + readonly #bmeWriter: BmeWriter; readonly #logger: LoggerService; readonly #moduleRegistry = buildModuleAddressRegistry(); readonly #typeIds = new Map(); @@ -60,6 +64,7 @@ export class BlockCommitterService { @inject(GovWriter) govWriter: GovWriter, @inject(AkashWriter) akashWriter: AkashWriter, @inject(ProviderWriter) providerWriter: ProviderWriter, + @inject(BmeWriter) bmeWriter: BmeWriter, @inject(LoggerService) logger: LoggerService ) { this.#db = db; @@ -68,6 +73,7 @@ export class BlockCommitterService { this.#govWriter = govWriter; this.#akashWriter = akashWriter; this.#providerWriter = providerWriter; + this.#bmeWriter = bmeWriter; this.#logger = logger; this.#logger.setContext("COMMITTER"); } @@ -136,7 +142,8 @@ export class BlockCommitterService { const balanceChanges = blocks.flatMap(block => deriveBalanceChanges(block, this.#moduleRegistry)); const accountTxs = blocks.flatMap(block => deriveAccountTxs(block)); const akashChanges = blocks.map(block => deriveAkashChanges(block)); - const accountIds = await this.#internAccounts(balanceChanges, accountTxs, akashChanges); + const bmeChanges = blocks.map(block => deriveBmeChanges(block)); + const accountIds = await this.#internAccounts(balanceChanges, accountTxs, akashChanges, bmeChanges); const balanceIntents = this.#resolveBalanceChanges(balanceChanges, accountIds); const accountTxRows = this.#resolveAccountTxs(accountTxs, accountIds); @@ -153,6 +160,7 @@ export class BlockCommitterService { await this.#govWriter.writeForBlocks(tx, blocks, accountIds); await this.#akashWriter.write(tx, akashChanges, accountIds); await this.#providerWriter.write(tx, akashChanges, accountIds); + await this.#bmeWriter.write(tx, bmeChanges, accountIds); await tx .insert(IndexerState) @@ -257,13 +265,15 @@ export class BlockCommitterService { /** * Interns every address the batch touches — spenders, receivers, correlated counterparties, tx signers, - * and the deployment owners/providers/depositors of the akash changes — on the base connection before - * the commit transaction, so the derived rows can reference their account ids by foreign key. + * the deployment owners/providers/depositors of the akash changes and the parties of the bme changes — + * on the base connection before the commit transaction, so the derived rows can reference their + * account ids by foreign key. */ async #internAccounts( balanceChanges: DerivedBalanceChange[], accountTxs: DerivedAccountTx[], - akashChanges: AkashBlockChanges[] + akashChanges: AkashBlockChanges[], + bmeChanges: BmeBlockChanges[] ): Promise> { const addresses = new Set(); @@ -279,13 +289,16 @@ export class BlockCommitterService { for (const address of collectAkashAddresses(akashChanges)) { addresses.add(address); } + for (const address of collectBmeAddresses(bmeChanges)) { + addresses.add(address); + } return this.#interner.resolve(addresses); } #resolveBalanceChanges(changes: DerivedBalanceChange[], accountIds: Map): ResolvedBalanceChange[] { return changes.map(change => ({ - accountId: this.#requireId(accountIds, change.address), + accountId: requireAccountId(accountIds, change.address), counterpartyAccountId: change.counterpartyAddress ? accountIds.get(change.counterpartyAddress) ?? null : null, denom: change.denom, delta: change.delta, @@ -297,15 +310,7 @@ export class BlockCommitterService { } #resolveAccountTxs(rows: DerivedAccountTx[], accountIds: Map): (typeof AccountTxs.$inferInsert)[] { - return rows.map(row => ({ accountId: this.#requireId(accountIds, row.address), height: row.height, txIndex: row.txIndex, role: row.role })); - } - - #requireId(accountIds: Map, address: string): number { - const accountId = accountIds.get(address); - if (accountId === undefined) { - throw new Error(`No interned account id for address ${address}`); - } - return accountId; + return rows.map(row => ({ accountId: requireAccountId(accountIds, row.address), height: row.height, txIndex: row.txIndex, role: row.role })); } async #internMessageTypes(blocks: DecodedBlock[]): Promise> { diff --git a/apps/chain-indexer/src/pipeline/block-decoder.service.spec.ts b/apps/chain-indexer/src/pipeline/block-decoder.service.spec.ts index ce62247cae..0437e2d278 100644 --- a/apps/chain-indexer/src/pipeline/block-decoder.service.spec.ts +++ b/apps/chain-indexer/src/pipeline/block-decoder.service.spec.ts @@ -234,6 +234,28 @@ describe(BlockDecoderService.name, () => { expect(decoded.blockEvents).toEqual([{ type: "coinbase", attributes: { minter: "akash1mint", amount: "10uakt" } }]); }); + it("captures bme lifecycle block events for the bme handler but drops vault funded ones", () => { + const { decoder } = setup(); + + const decoded = decoder.decode( + buildBlock({ txs: [] }), + buildBlockResults([], { + finalize_block_events: [ + event("akash.bme.v1.EventLedgerRecordExecuted", { id: '{"denom":"uakt","to_denom":"uact","source":"bme","height":100,"sequence":1}' }), + event("akash.bme.v1.EventMintStatusChange", { previous_status: '"mint_status_healthy"', new_status: '"mint_status_warning"' }), + event("akash.bme.v1.EventLedgerRecordCanceled", { cancel_reason: '"epsilon"' }), + event("akash.bme.v1.EventVaultFunded", { source: '"bme"' }) + ] + }) + ); + + expect(decoded.blockEvents.map(e => e.type)).toEqual([ + "akash.bme.v1.EventLedgerRecordExecuted", + "akash.bme.v1.EventMintStatusChange", + "akash.bme.v1.EventLedgerRecordCanceled" + ]); + }); + it("falls back to begin and end block events when finalize is absent", () => { const { decoder } = setup(); diff --git a/apps/chain-indexer/src/pipeline/block-decoder.service.ts b/apps/chain-indexer/src/pipeline/block-decoder.service.ts index 08bade8bb2..2561ee8370 100644 --- a/apps/chain-indexer/src/pipeline/block-decoder.service.ts +++ b/apps/chain-indexer/src/pipeline/block-decoder.service.ts @@ -19,8 +19,10 @@ import type { RpcBlockResult, RpcBlockResultsResult, RpcEvent, RpcTxResult } fro * The ledger derives balances and reasons from the coin/transfer/mint/burn/slash events; the gov events carry * proposal ids and lifecycle transitions for the governance handler; the akash events catch deployment and * lease closes that happen as side effects of other messages (group close, overdraw) — `akash.v1` is the - * legacy string-attribute event of early mainnet, the typed events are the current chain's. Capturing the - * rest would waste memory across backfill batches. + * legacy string-attribute event of early mainnet, the typed events are the current chain's. The bme events + * carry the executed/canceled ledger records and mint status transitions the BME handler derives from + * (EventVaultFunded is deliberately absent: the vault's balance is already exact in the balance ledger). + * Capturing the rest would waste memory across backfill batches. */ const RELEVANT_EVENT_TYPES = new Set([ "coin_spent", @@ -34,7 +36,10 @@ const RELEVANT_EVENT_TYPES = new Set([ "inactive_proposal", "akash.v1", "akash.deployment.v1.EventDeploymentClosed", - "akash.market.v1.EventLeaseClosed" + "akash.market.v1.EventLeaseClosed", + "akash.bme.v1.EventLedgerRecordExecuted", + "akash.bme.v1.EventMintStatusChange", + "akash.bme.v1.EventLedgerRecordCanceled" ]); const MSG_INDEX_ATTRIBUTE = "msg_index";