From c3c94fdd3083bc9f1fe63bfe8ffb776304fca854 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini <48031343+ecioppettini@users.noreply.github.com> Date: Wed, 1 May 2024 22:12:33 -0300 Subject: [PATCH] conway governance tasks (#181) * drep tasks and sancho support * drep delegation for adddress and votes for address endpoints * use a credential instead of bech32 address * votes for action ids endpoint * voting task: avoid insert when empty * all votes endpoint: rename and fix doc * add transaction history drep filters * update cml (to non merged branch) * fix route name inconsistencies and wrong tx id format * fix VoteDelegCert credential format * multiera address task: implement the combined certificates * multiera address delegation: add new certificates * multiera_drep_delegation add combined certs * multiera address: clippy * cardano sink: handle conway era * cardano net setting: handle sancho * fix multiera_drep_delegation not using drep credential * Bump to cml 5.3.0 --------- Co-authored-by: Sebastien Guillemot --- Cargo.lock | 46 ++--- Cargo.toml | 7 + indexer/Cargo.toml | 8 +- indexer/entity/src/governance_votes.rs | 25 +++ indexer/entity/src/lib.rs | 3 + indexer/entity/src/prelude.rs | 5 + indexer/entity/src/stake_delegation_drep.rs | 37 ++++ indexer/genesis/sanchonet-byron-genesis.json | 62 +++++++ indexer/migration/src/lib.rs | 4 + ...326_000020_create_drep_delegation_table.rs | 75 ++++++++ ...6_000021_create_governance_voting_table.rs | 66 +++++++ indexer/reparse/Cargo.toml | 8 +- indexer/src/genesis.rs | 4 +- indexer/src/main.rs | 1 + indexer/src/sinks/cardano.rs | 2 +- indexer/src/sources/oura_source.rs | 26 ++- indexer/tasks/Cargo.toml | 8 +- indexer/tasks/src/byron/byron_txs.rs | 2 +- indexer/tasks/src/genesis/genesis_txs.rs | 2 +- indexer/tasks/src/multiera/dex/common.rs | 5 +- indexer/tasks/src/multiera/dex/minswap_v1.rs | 6 +- .../tasks/src/multiera/dex/sundaeswap_v1.rs | 6 +- .../tasks/src/multiera/dex/wingriders_v1.rs | 41 ++-- indexer/tasks/src/multiera/mod.rs | 2 + .../tasks/src/multiera/multiera_address.rs | 175 ++++++++++++++++-- .../multiera/multiera_address_delegation.rs | 10 + .../tasks/src/multiera/multiera_asset_utxo.rs | 13 +- indexer/tasks/src/multiera/multiera_datum.rs | 23 +-- .../src/multiera/multiera_drep_delegation.rs | 132 +++++++++++++ .../multiera/multiera_governance_voting.rs | 59 ++++++ .../src/multiera/multiera_projected_nft.rs | 49 +++-- .../src/multiera/multiera_reference_inputs.rs | 19 +- .../multiera/multiera_stake_credentials.rs | 20 +- indexer/tasks/src/multiera/multiera_txs.rs | 2 +- .../src/multiera/multiera_unused_input.rs | 19 +- .../src/multiera/multiera_used_inputs.rs | 13 +- indexer/tasks/src/multiera/utils/common.rs | 13 +- indexer/tasks/src/types.rs | 6 + .../DrepDelegationForAddressController.ts | 72 +++++++ .../GovernanceVotesByActionIdsController.ts | 88 +++++++++ .../GovernanceVotesForCredentialController.ts | 83 +++++++++ .../drepDelegationForAddress.queries.ts | 41 ++++ .../delegation/drepDelegationForAddress.sql | 11 ++ .../governance/votesForAddress.queries.ts | 97 ++++++++++ .../app/models/governance/votesForAddress.sql | 36 ++++ .../app/services/DrepDelegationForAddress.ts | 10 + .../GovernanceCredentialVotesByActionIds.ts | 24 +++ .../app/services/GovernanceVotesForAddress.ts | 25 +++ webserver/shared/constants.ts | 8 + webserver/shared/errors.ts | 6 + .../shared/models/DelegationForAddress.ts | 5 + webserver/shared/models/Governance.ts | 25 +++ webserver/shared/models/common.ts | 3 + webserver/shared/routes.ts | 35 +++- 54 files changed, 1433 insertions(+), 140 deletions(-) create mode 100644 indexer/entity/src/governance_votes.rs create mode 100644 indexer/entity/src/stake_delegation_drep.rs create mode 100644 indexer/genesis/sanchonet-byron-genesis.json create mode 100644 indexer/migration/src/m20240326_000020_create_drep_delegation_table.rs create mode 100644 indexer/migration/src/m20240326_000021_create_governance_voting_table.rs create mode 100644 indexer/tasks/src/multiera/multiera_drep_delegation.rs create mode 100644 indexer/tasks/src/multiera/multiera_governance_voting.rs create mode 100644 webserver/server/app/controllers/DrepDelegationForAddressController.ts create mode 100644 webserver/server/app/controllers/GovernanceVotesByActionIdsController.ts create mode 100644 webserver/server/app/controllers/GovernanceVotesForCredentialController.ts create mode 100644 webserver/server/app/models/delegation/drepDelegationForAddress.queries.ts create mode 100644 webserver/server/app/models/delegation/drepDelegationForAddress.sql create mode 100644 webserver/server/app/models/governance/votesForAddress.queries.ts create mode 100644 webserver/server/app/models/governance/votesForAddress.sql create mode 100644 webserver/server/app/services/DrepDelegationForAddress.ts create mode 100644 webserver/server/app/services/GovernanceCredentialVotesByActionIds.ts create mode 100644 webserver/server/app/services/GovernanceVotesForAddress.ts create mode 100644 webserver/shared/models/Governance.ts diff --git a/Cargo.lock b/Cargo.lock index e7cf9a75..1ae687ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,9 +584,9 @@ dependencies = [ "anyhow", "async-trait", "clap 3.2.25", - "cml-chain 5.2.0", - "cml-core 5.2.0", - "cml-crypto 5.2.0", + "cml-chain 5.3.0", + "cml-core 5.3.0", + "cml-crypto 5.3.0", "cml-multi-era", "ctrlc", "dcspark-blockchain-source", @@ -765,15 +765,15 @@ dependencies = [ [[package]] name = "cml-chain" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec309ed37a653542db58bf506bf31f59f942a5cf269c1709d9555b699a1b302a" +checksum = "b58dce9d0eb50807b73ea4c130549347b40580b96b2f199650b9845566e0b5ef" dependencies = [ "base64 0.21.7", "bech32 0.7.3", "cbor_event", - "cml-core 5.2.0", - "cml-crypto 5.2.0", + "cml-core 5.3.0", + "cml-crypto 5.3.0", "derivative", "fraction", "getrandom", @@ -820,9 +820,9 @@ dependencies = [ [[package]] name = "cml-core" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c826dd7e8939826e1090ebbef48dce630bbceb2a4a4aadbdccd6b583bb7d02" +checksum = "c9cbdbd4f17a5df89ec8e65d01ace223ea01c0501d705629966df466cecb7d10" dependencies = [ "base64 0.13.1", "bech32 0.7.3", @@ -870,15 +870,15 @@ dependencies = [ [[package]] name = "cml-crypto" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e79a9a6614c1fce917db76eda4ba44fc9a3d8ee5d43becf66299e89e04a8a0f" +checksum = "34119f9c40c11fabaeba5bc3b20290ba0e27461aab003c6813a8568a44252daf" dependencies = [ "base64 0.21.7", "bech32 0.7.3", "cbor_event", "cfg-if 1.0.0", - "cml-core 5.2.0", + "cml-core 5.3.0", "cryptoxide", "derivative", "digest 0.9.0", @@ -894,15 +894,15 @@ dependencies = [ [[package]] name = "cml-multi-era" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e71d10d6eb5afaa9c2befec03827defea3929b455166154ab8bc25d6dc717767" +checksum = "bc958024de3d1885c5e13ca4cbc1c9f64e89662aed474775cb2defc556e81bc3" dependencies = [ "bech32 0.7.3", "cbor_event", - "cml-chain 5.2.0", - "cml-core 5.2.0", - "cml-crypto 5.2.0", + "cml-chain 5.3.0", + "cml-core 5.3.0", + "cml-crypto 5.3.0", "derivative", "hex", "linked-hash-map", @@ -2852,9 +2852,9 @@ name = "reparse" version = "0.1.0" dependencies = [ "anyhow", - "cml-chain 5.2.0", - "cml-core 5.2.0", - "cml-crypto 5.2.0", + "cml-chain 5.3.0", + "cml-core 5.3.0", + "cml-crypto 5.3.0", "cml-multi-era", "dotenv", "entity", @@ -3741,9 +3741,9 @@ dependencies = [ "anyhow", "cardano-projected-nft", "cfg-if 1.0.0", - "cml-chain 5.2.0", - "cml-core 5.2.0", - "cml-crypto 5.2.0", + "cml-chain 5.3.0", + "cml-core 5.3.0", + "cml-crypto 5.3.0", "cml-multi-era", "cryptoxide", "entity", diff --git a/Cargo.toml b/Cargo.toml index 1306f3eb..5aa91926 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,10 @@ members = [ "indexer/plan-visualizer", "indexer/task-docgen" ] + + +[workspace.dependencies] +cml-chain = { version = "5.3.0" } +cml-core = { version = "5.3.0" } +cml-crypto = { version = "5.3.0" } +cml-multi-era = { version = "5.3.0" } diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml index 049cd77b..eee4fdad 100644 --- a/indexer/Cargo.toml +++ b/indexer/Cargo.toml @@ -21,10 +21,10 @@ tasks = { path = "tasks" } # [indexer] anyhow = { version = "1.0.69" } async-trait = { version = "0.1.64" } -cml-chain = { version = "5.2.0" } -cml-core = { version = "5.2.0" } -cml-crypto = { version = "5.2.0" } -cml-multi-era = { version = "5.2.0" } +cml-chain = { workspace = true } +cml-core = { workspace = true } +cml-crypto = { workspace = true } +cml-multi-era = { workspace = true } clap = { version = "3.1", features = ["derive"] } ctrlc = { version = "3.2.4", features = ["termination"] } dotenv = { version = "0.15.0" } diff --git a/indexer/entity/src/governance_votes.rs b/indexer/entity/src/governance_votes.rs new file mode 100644 index 00000000..e30d3a19 --- /dev/null +++ b/indexer/entity/src/governance_votes.rs @@ -0,0 +1,25 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "GovernanceVote")] +pub struct Model { + #[sea_orm(primary_key, column_type = "BigInteger")] + pub id: i64, + pub tx_id: i64, + pub voter: Vec, + pub gov_action_id: Vec, + pub vote: Vec, +} + +#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::transaction::Entity", + from = "Column::TxId", + to = "super::transaction::Column::Id" + )] + Transaction, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/indexer/entity/src/lib.rs b/indexer/entity/src/lib.rs index f22250d5..136153ef 100644 --- a/indexer/entity/src/lib.rs +++ b/indexer/entity/src/lib.rs @@ -13,9 +13,12 @@ pub mod asset_mint; pub mod asset_utxos; pub mod cip25_entry; pub mod dex_swap; +pub mod governance_votes; pub mod native_asset; pub mod plutus_data; pub mod plutus_data_hash; pub mod projected_nft; +// todo: rename to pool? pub mod stake_delegation; +pub mod stake_delegation_drep; pub mod transaction_metadata; diff --git a/indexer/entity/src/prelude.rs b/indexer/entity/src/prelude.rs index efe0ccb2..ee2e1a58 100644 --- a/indexer/entity/src/prelude.rs +++ b/indexer/entity/src/prelude.rs @@ -27,6 +27,11 @@ pub use super::dex_swap::{ ActiveModel as DexSwapActiveModel, Column as DexSwapColumn, Entity as DexSwap, Model as DexSwapModel, PrimaryKey as DexSwapPrimaryKey, Relation as DexSwapRelation, }; +pub use super::governance_votes::{ + ActiveModel as GovernanceVoteActiveModel, Column as GovernanceVoteColumn, + Entity as GovernanceVote, Model as GovernanceVoteModel, PrimaryKey as GovernanceVotePrimaryKey, + Relation as GovernanceVoteRelation, +}; pub use super::native_asset::{ ActiveModel as NativeAssetActiveModel, Column as NativeAssetColumn, Entity as NativeAsset, Model as NativeAssetModel, PrimaryKey as NativeAssetPrimaryKey, diff --git a/indexer/entity/src/stake_delegation_drep.rs b/indexer/entity/src/stake_delegation_drep.rs new file mode 100644 index 00000000..51bc7133 --- /dev/null +++ b/indexer/entity/src/stake_delegation_drep.rs @@ -0,0 +1,37 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "StakeDelegationDrepCredentialRelation")] +pub struct Model { + #[sea_orm(primary_key, column_type = "BigInteger")] + pub id: i64, + pub stake_credential: i64, + pub drep_credential: Option>, + pub tx_id: i64, + pub previous_drep_credential: Option>, +} + +#[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::stake_credential::Entity", + from = "Column::StakeCredential", + to = "super::stake_credential::Column::Id" + )] + StakeCredential, + #[sea_orm( + belongs_to = "super::transaction::Entity", + from = "Column::TxId", + to = "super::transaction::Column::Id" + )] + Transaction, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::StakeCredential.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/indexer/genesis/sanchonet-byron-genesis.json b/indexer/genesis/sanchonet-byron-genesis.json new file mode 100644 index 00000000..f0c29c65 --- /dev/null +++ b/indexer/genesis/sanchonet-byron-genesis.json @@ -0,0 +1,62 @@ +{ + "avvmDistr": {}, + "blockVersionData": { + "heavyDelThd": "300000000000", + "maxBlockSize": "2000000", + "maxHeaderSize": "2000000", + "maxProposalSize": "700", + "maxTxSize": "4096", + "mpcThd": "20000000000000", + "scriptVersion": 0, + "slotDuration": "20000", + "softforkRule": { + "initThd": "900000000000000", + "minThd": "600000000000000", + "thdDecrement": "50000000000000" + }, + "txFeePolicy": { + "multiplier": "43946000000", + "summand": "155381000000000" + }, + "unlockStakeEpoch": "18446744073709551615", + "updateImplicit": "10000", + "updateProposalThd": "100000000000000", + "updateVoteThd": "1000000000000" + }, + "bootStakeholders": { + "318488dc356f6034104804b2cb6a2dcc055202491386fb0d5af7c3ba": 1, + "3a3c2ffaf066c8f211a1bdfd844f767ac453b1d94915e725c5867467": 1, + "3ae8eabb4e0626cea0ba38d8303d59514dae9c307d93bad3d259e4a9": 1 + }, + "heavyDelegation": { + "318488dc356f6034104804b2cb6a2dcc055202491386fb0d5af7c3ba": { + "cert": "b80e06679023284236df3464dc6aab3f56f23cb721d5943c59632ac77004f76ae415b6d291606c7194509e1fefa0c8341eed269bd0e0e1433302b00912a4230c", + "delegatePk": "9ELoyHN4GVtXrFzAJZApAVjrhwftqEFVoDXl9ebtTwpe/lG4b5ZkgH3DqwHE1hNJFRsnYs4zYzMmdbnoR7lfUA==", + "issuerPk": "MHFL9SqIV6KuXSAvp08jHBRtHwNsDJMsCxbmXLorSbfLAORg7waqVL8NEaKU3Lb0FBIX5sHVC21i1M/c0jrnlA==", + "omega": 0 + }, + "3a3c2ffaf066c8f211a1bdfd844f767ac453b1d94915e725c5867467": { + "cert": "ce91b8e35b67de2236fa79b353d1c4ebd97ad4b4cc89056a1acfc217ece8e91fbffc4bf44604a96a1064c9997f6cd39b81284aadfac752056eafc6b5996a6509", + "delegatePk": "Grpf6iTqd9aWc3QWvfthNv2l8Pp0X2tKpoIoPn0+Dy1+ow60UTu9i1j4KPjp1uzrnM4JoUcmkCGF507fPagO8w==", + "issuerPk": "1FPA7qSOPVDlNZoQAuoB2dnm+tKI5td6+BO5sJ2rswVxuS6S6sjBFVfVz/VXfKTcEt/AKyffgzWXAtPCnhC1jw==", + "omega": 0 + }, + "3ae8eabb4e0626cea0ba38d8303d59514dae9c307d93bad3d259e4a9": { + "cert": "908dd25262598050d60cb24928a7059fea3726a1dd7764645edab654d3b4e37ba69acd4841454f70f0f643305ede0ef66dc0ea9747a2387da05d2af77963f30a", + "delegatePk": "1zYduiReianx6HJHgQqtira7XY6M/Ol4tFj/O7TzTLcNfgazJm8pq5y6HAANwl91iL1pDZuIgFjzI+2i1Z6y2Q==", + "issuerPk": "Pgj3IyTJDyxr+t5fcMuM3aPtyNCxOo4T9sr78BNbgWBGwlTGo0P6UtzNLyqloLsH8V6Lv6kYMdWELAiEyfCpkw==", + "omega": 0 + } + }, + "nonAvvmBalances": { + "FHnt4NL7yPXqn7xha3WB99wYLxAc1FhceD3D1pQWaCthk9RYB46aGb6Tbq2KxV5": "0", + "FHnt4NL7yPXwj8m191s48v1RZtQqA2sVHpamzStuXTuAnzYUSR6hRPqhYmW3MY4": "0", + "FHnt4NL7yPXzVZ5xexcb7rWqCYWuFU7y6Pp4tLTiv6txhDcpQ2m7AFGMirsi1F1": "30000000000000000", + "FHnt4NL7yPY27r794z4UiYJ3RwezucDRLX94Pzy6mYPNUNWboB71S9xUm2WEDrv": "0" + }, + "protocolConsts": { + "k": 432, + "protocolMagic": 4 + }, + "startTime": 1686789000 +} diff --git a/indexer/migration/src/lib.rs b/indexer/migration/src/lib.rs index fa57a67c..ddde20b0 100644 --- a/indexer/migration/src/lib.rs +++ b/indexer/migration/src/lib.rs @@ -21,6 +21,8 @@ mod m20230927_000016_create_stake_delegation_table; mod m20231025_000017_projected_nft; mod m20231220_000018_asset_utxo_table; mod m20240229_000019_add_block_tx_count_column; +mod m20240326_000020_create_drep_delegation_table; +mod m20240326_000021_create_governance_voting_table; pub struct Migrator; @@ -49,6 +51,8 @@ impl MigratorTrait for Migrator { Box::new(m20231025_000017_projected_nft::Migration), Box::new(m20231220_000018_asset_utxo_table::Migration), Box::new(m20240229_000019_add_block_tx_count_column::Migration), + Box::new(m20240326_000020_create_drep_delegation_table::Migration), + Box::new(m20240326_000021_create_governance_voting_table::Migration), ] } } diff --git a/indexer/migration/src/m20240326_000020_create_drep_delegation_table.rs b/indexer/migration/src/m20240326_000020_create_drep_delegation_table.rs new file mode 100644 index 00000000..15409f64 --- /dev/null +++ b/indexer/migration/src/m20240326_000020_create_drep_delegation_table.rs @@ -0,0 +1,75 @@ +use entity::prelude::{StakeCredential, StakeCredentialColumn, Transaction, TransactionColumn}; +use entity::stake_delegation_drep::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20240326_000020_create_drep_delegation_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .big_integer() + .not_null() + .auto_increment(), + ) + .col( + ColumnDef::new(Column::StakeCredential) + .big_integer() + .not_null(), + ) + .col(ColumnDef::new(Column::TxId).big_integer().not_null()) + .col(ColumnDef::new(Column::DrepCredential).binary()) + .col(ColumnDef::new(Column::PreviousDrepCredential).binary()) + .foreign_key( + ForeignKey::create() + .name("fk-stake_delegation_drep-credential_id") + .from(Entity, Column::StakeCredential) + .to(StakeCredential, StakeCredentialColumn::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-stake_delegation_drep-tx_id") + .from(Entity, Column::TxId) + .to(Transaction, TransactionColumn::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .primary_key( + Index::create() + .table(Entity) + .name("stake_delegation_drep_credential-pk") + .col(Column::Id), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Entity) + .name("index-stake_delegation_credential_drep-stake_credential") + .col(Column::StakeCredential) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/indexer/migration/src/m20240326_000021_create_governance_voting_table.rs b/indexer/migration/src/m20240326_000021_create_governance_voting_table.rs new file mode 100644 index 00000000..80354361 --- /dev/null +++ b/indexer/migration/src/m20240326_000021_create_governance_voting_table.rs @@ -0,0 +1,66 @@ +use entity::governance_votes::*; +use entity::prelude::{Transaction, TransactionColumn}; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20240326_000021_create_governance_vote_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .big_integer() + .not_null() + .auto_increment(), + ) + .col(ColumnDef::new(Column::TxId).big_integer().not_null()) + .col(ColumnDef::new(Column::Voter).binary()) + .col(ColumnDef::new(Column::GovActionId).binary()) + .col(ColumnDef::new(Column::Vote).binary()) + .foreign_key( + ForeignKey::create() + .name("fk-governance_vote-tx_id") + .from(Entity, Column::TxId) + .to(Transaction, TransactionColumn::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .primary_key( + Index::create() + .table(Entity) + .name("governance_vote-pk") + .col(Column::Id), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Entity) + .name("index-governance_vote-voter") + .col(Column::Voter) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/indexer/reparse/Cargo.toml b/indexer/reparse/Cargo.toml index 305c07c3..25d38b57 100644 --- a/indexer/reparse/Cargo.toml +++ b/indexer/reparse/Cargo.toml @@ -10,10 +10,10 @@ entity = { path = "../entity" } # [reparse] anyhow = { version = "1.0.69" } -cml-chain = { version = "5.2.0" } -cml-core = { version = "5.2.0" } -cml-crypto = { version = "5.2.0" } -cml-multi-era = { version = "5.2.0" } +cml-chain = { workspace = true } +cml-core = { workspace = true } +cml-crypto = { workspace = true } +cml-multi-era = { workspace = true } dotenv = { version = "0.15.0" } futures = { version = "0.3.21" } hex = { version = "0.4.0" } diff --git a/indexer/src/genesis.rs b/indexer/src/genesis.rs index 8d5ed3cc..47e1e801 100644 --- a/indexer/src/genesis.rs +++ b/indexer/src/genesis.rs @@ -17,6 +17,7 @@ const GENESIS_MAINNET: &str = "./genesis/mainnet-byron-genesis.json"; const GENESIS_PREVIEW: &str = "./genesis/preview-byron-genesis.json"; const GENESIS_PREPROD: &str = "./genesis/preprod-byron-genesis.json"; const GENESIS_TESTNET: &str = "./genesis/testnet-byron-genesis.json"; +const GENESIS_SANCHONET: &str = "./genesis/sanchonet-byron-genesis.json"; pub async fn process_genesis( conn: &DatabaseConnection, @@ -24,11 +25,12 @@ pub async fn process_genesis( exec_plan: Arc, ) -> anyhow::Result<()> { // https://github.com/txpipe/oura/blob/67b01e8739ed2927ced270e08daea74b03bcc7f7/src/sources/common.rs#L91 - let genesis_path = match dbg!(network) { + let genesis_path = match network { "mainnet" => GENESIS_MAINNET, "testnet" => GENESIS_TESTNET, "preview" => GENESIS_PREVIEW, "preprod" => GENESIS_PREPROD, + "sanchonet" => GENESIS_SANCHONET, rest => { return Err(anyhow!( "{} is invalid. NETWORK must be either mainnet/preview/preprod/testnet", diff --git a/indexer/src/main.rs b/indexer/src/main.rs index 05bc965d..bd6284b5 100644 --- a/indexer/src/main.rs +++ b/indexer/src/main.rs @@ -184,6 +184,7 @@ async fn main() -> anyhow::Result<()> { "mainnet" => dcspark_blockchain_source::cardano::NetworkConfiguration::mainnet(), "preprod" => dcspark_blockchain_source::cardano::NetworkConfiguration::preprod(), "preview" => dcspark_blockchain_source::cardano::NetworkConfiguration::preview(), + "sanchonet" => dcspark_blockchain_source::cardano::NetworkConfiguration::sancho(), _ => return Err(anyhow::anyhow!("network not supported by source")), }; diff --git a/indexer/src/sinks/cardano.rs b/indexer/src/sinks/cardano.rs index 7b45cc64..b394ea3a 100644 --- a/indexer/src/sinks/cardano.rs +++ b/indexer/src/sinks/cardano.rs @@ -263,7 +263,7 @@ fn to_era_value(x: &MultiEraBlock) -> EraValue { MultiEraBlock::Mary(_) => EraValue::Mary, MultiEraBlock::Alonzo(_) => EraValue::Alonzo, MultiEraBlock::Babbage(_) => EraValue::Babbage, - _ => unreachable!("all known eras are handled"), + MultiEraBlock::Conway(_) => EraValue::Conway, } } diff --git a/indexer/src/sources/oura_source.rs b/indexer/src/sources/oura_source.rs index 92737719..b868af28 100644 --- a/indexer/src/sources/oura_source.rs +++ b/indexer/src/sources/oura_source.rs @@ -143,10 +143,30 @@ fn oura_bootstrap( network: &str, socket: String, ) -> anyhow::Result<(Vec>, StageReceiver)> { - let magic = MagicArg::from_str(network).map_err(|_| anyhow!("magic arg failed"))?; + let magic = match network { + "sanchonet" => MagicArg(4), + _ => MagicArg::from_str(network).map_err(|_| anyhow!("magic arg failed"))?, + }; - let well_known = ChainWellKnownInfo::try_from_magic(*magic) - .map_err(|_| anyhow!("chain well known info failed"))?; + let well_known = if magic.0 == 4 { + ChainWellKnownInfo { + byron_epoch_length: 86400, + byron_slot_length: 20, + byron_known_slot: 0, + byron_known_hash: "".to_string(), + byron_known_time: 1686789000, + shelley_epoch_length: 86400, + shelley_slot_length: 1, + shelley_known_slot: 0, + shelley_known_hash: "".to_string(), + shelley_known_time: 1686789000, + address_hrp: "addr_test".to_string(), + adahandle_policy: "".to_string(), + } + } else { + ChainWellKnownInfo::try_from_magic(*magic) + .map_err(|_| anyhow!("chain well known info failed"))? + }; let utils = Arc::new(Utils::new(well_known)); diff --git a/indexer/tasks/Cargo.toml b/indexer/tasks/Cargo.toml index 27e68a44..c4bc6a4d 100644 --- a/indexer/tasks/Cargo.toml +++ b/indexer/tasks/Cargo.toml @@ -12,10 +12,10 @@ entity = { path = "../entity" } # [tasks] anyhow = { version = "1.0.69" } -cml-chain = { version = "5.2.0" } -cml-core = { version = "5.2.0" } -cml-crypto = { version = "5.2.0" } -cml-multi-era = { version = "5.2.0" } +cml-chain = { workspace = true } +cml-core = { workspace = true } +cml-crypto = { workspace = true } +cml-multi-era = { workspace = true } cardano-projected-nft = { git = "https://github.com/dcSpark/projected-nft-whirlpool.git", rev = "13f81e8666743fefd14c5e1affb1cd828d8c473b" } cfg-if = { version = "1.0.0" } cryptoxide = { version = "0.4.2" } diff --git a/indexer/tasks/src/byron/byron_txs.rs b/indexer/tasks/src/byron/byron_txs.rs index 2befec4c..99187290 100644 --- a/indexer/tasks/src/byron/byron_txs.rs +++ b/indexer/tasks/src/byron/byron_txs.rs @@ -23,7 +23,7 @@ carp_task! { execute |previous_data, task| handle_tx( task.db_tx, task.block, - &previous_data.byron_block.as_ref().unwrap(), + previous_data.byron_block.as_ref().unwrap(), task.config.readonly, task.config.include_payload ); diff --git a/indexer/tasks/src/genesis/genesis_txs.rs b/indexer/tasks/src/genesis/genesis_txs.rs index 9e9db956..f12af54f 100644 --- a/indexer/tasks/src/genesis/genesis_txs.rs +++ b/indexer/tasks/src/genesis/genesis_txs.rs @@ -34,7 +34,7 @@ carp_task! { execute |previous_data, task| handle_txs( task.db_tx, task.block, - &previous_data.genesis_block.as_ref().unwrap(), + previous_data.genesis_block.as_ref().unwrap(), task.config.include_payload ); merge_result |previous_data, result| { diff --git a/indexer/tasks/src/multiera/dex/common.rs b/indexer/tasks/src/multiera/dex/common.rs index ef2d4ae6..afdfbefd 100644 --- a/indexer/tasks/src/multiera/dex/common.rs +++ b/indexer/tasks/src/multiera/dex/common.rs @@ -9,6 +9,7 @@ use cml_chain::json::plutus_datums::{ decode_plutus_datum_to_json_str, decode_plutus_datum_to_json_value, CardanoNodePlutusDatumSchema, }; +use cml_chain::NonemptySetPlutusData; use entity::dex_swap::Operation; use entity::sea_orm::{DatabaseTransaction, Set}; use std::collections::{BTreeMap, BTreeSet}; @@ -18,7 +19,7 @@ use std::collections::{BTreeMap, BTreeSet}; pub fn filter_outputs_and_datums_by_hash( outputs: &[cml_multi_era::utils::MultiEraTransactionOutput], payment_hashes: &[&str], - plutus_data: &[cml_chain::plutus::PlutusData], + plutus_data: &Option, ) -> Vec<( cml_multi_era::utils::MultiEraTransactionOutput, cml_chain::plutus::PlutusData, @@ -41,7 +42,7 @@ pub fn filter_outputs_and_datums_by_hash( pub fn filter_outputs_and_datums_by_address( outputs: &[cml_multi_era::utils::MultiEraTransactionOutput], addresses: &[&str], - plutus_data: &[cml_chain::plutus::PlutusData], + plutus_data: &Option, ) -> Vec<( cml_multi_era::utils::MultiEraTransactionOutput, cml_chain::plutus::PlutusData, diff --git a/indexer/tasks/src/multiera/dex/minswap_v1.rs b/indexer/tasks/src/multiera/dex/minswap_v1.rs index 604d01f7..d3c67137 100644 --- a/indexer/tasks/src/multiera/dex/minswap_v1.rs +++ b/indexer/tasks/src/multiera/dex/minswap_v1.rs @@ -33,7 +33,7 @@ impl Dex for MinSwapV1 { if let Some((output, datum)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH1, POOL_SCRIPT_HASH2], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { @@ -77,7 +77,7 @@ impl Dex for MinSwapV1 { if let Some((main_output, main_datum)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH1, POOL_SCRIPT_HASH2], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { @@ -109,7 +109,7 @@ impl Dex for MinSwapV1 { for (input, input_datum) in filter_outputs_and_datums_by_address( &inputs, &[BATCH_ORDER_ADDRESS1, BATCH_ORDER_ADDRESS2], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) { let input_datum = datum_to_json(&input_datum)?; diff --git a/indexer/tasks/src/multiera/dex/sundaeswap_v1.rs b/indexer/tasks/src/multiera/dex/sundaeswap_v1.rs index ec6b4a87..ccc6bf07 100644 --- a/indexer/tasks/src/multiera/dex/sundaeswap_v1.rs +++ b/indexer/tasks/src/multiera/dex/sundaeswap_v1.rs @@ -33,7 +33,7 @@ impl Dex for SundaeSwapV1 { if let Some((output, datum)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { @@ -77,7 +77,7 @@ impl Dex for SundaeSwapV1 { if let Some((main_output, main_datum)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { @@ -109,7 +109,7 @@ impl Dex for SundaeSwapV1 { for (input, input_datum) in filter_outputs_and_datums_by_hash( &inputs, &[REQUEST_SCRIPT_HASH], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums.clone(), ) { let input_datum = datum_to_json(&input_datum)?; diff --git a/indexer/tasks/src/multiera/dex/wingriders_v1.rs b/indexer/tasks/src/multiera/dex/wingriders_v1.rs index 28563ee4..dd03e78d 100644 --- a/indexer/tasks/src/multiera/dex/wingriders_v1.rs +++ b/indexer/tasks/src/multiera/dex/wingriders_v1.rs @@ -1,4 +1,5 @@ use cml_chain::byron::ByronTxOut; +use cml_chain::plutus::LegacyRedeemer; use cml_core::serialization::{FromBytes, Serialize}; use cml_crypto::RawBytesEncoding; use cml_multi_era::utils::MultiEraTransactionOutput; @@ -36,7 +37,7 @@ impl Dex for WingRidersV1 { if let Some((output, datum)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { @@ -91,12 +92,33 @@ impl Dex for WingRidersV1 { if let Some((pool_output, _)) = filter_outputs_and_datums_by_hash( &tx.outputs(), &[POOL_SCRIPT_HASH], - &tx_witness.plutus_datums.clone().unwrap_or_default(), + &tx_witness.plutus_datums, ) .first() { let redeemers = tx_witness.redeemers.clone().ok_or("No redeemers")?; + let redeemers = match redeemers { + cml_chain::plutus::Redeemers::ArrLegacyRedeemer { + arr_legacy_redeemer, + arr_legacy_redeemer_encoding: _, + } => arr_legacy_redeemer, + cml_chain::plutus::Redeemers::MapRedeemerKeyToRedeemerVal { + map_redeemer_key_to_redeemer_val, + map_redeemer_key_to_redeemer_val_encoding: _, + } => map_redeemer_key_to_redeemer_val + .take() + .into_iter() + .map(|(key, val)| LegacyRedeemer { + tag: key.tag, + index: key.index, + data: val.data, + ex_units: val.ex_units, + encodings: None, + }) + .collect(), + }; + // Get pool input from redemeers let pool_input_redeemer = redeemers.first().ok_or("No redeemers")?; let pool_input = datum_to_json(&pool_input_redeemer.data)?["fields"][0]["int"] @@ -138,11 +160,9 @@ impl Dex for WingRidersV1 { let input = inputs.get(redeemer).ok_or("Failed to pair output")?.clone(); // get information about swap from pool plutus data - let parent_datum = get_plutus_datum_for_output( - &inputs[parent], - &tx_witness.plutus_datums.clone().unwrap_or_default(), - ) - .unwrap(); + let parent_datum = + get_plutus_datum_for_output(&inputs[parent], &tx_witness.plutus_datums) + .unwrap(); let parent_datum = datum_to_json(&parent_datum)?; @@ -158,11 +178,8 @@ impl Dex for WingRidersV1 { let asset2 = build_asset(parse_asset_item(1, 0)?, parse_asset_item(1, 1)?); // get actual plutus datum - let input_datum = get_plutus_datum_for_output( - &input, - &tx_witness.plutus_datums.clone().unwrap_or_default(), - ) - .unwrap(); + let input_datum = + get_plutus_datum_for_output(&input, &tx_witness.plutus_datums).unwrap(); let input_datum = datum_to_json(&input_datum)?; // identify operation: 0 = swap let operation = input_datum["fields"][1]["constructor"] diff --git a/indexer/tasks/src/multiera/mod.rs b/indexer/tasks/src/multiera/mod.rs index 9ad6a302..dcd78b7f 100644 --- a/indexer/tasks/src/multiera/mod.rs +++ b/indexer/tasks/src/multiera/mod.rs @@ -7,7 +7,9 @@ pub mod multiera_asset_utxo; pub mod multiera_block; pub mod multiera_cip25entry; pub mod multiera_datum; +pub mod multiera_drep_delegation; pub mod multiera_executor; +pub mod multiera_governance_voting; pub mod multiera_metadata; pub mod multiera_minswap_v1_mean_price; pub mod multiera_minswap_v1_swap; diff --git a/indexer/tasks/src/multiera/multiera_address.rs b/indexer/tasks/src/multiera/multiera_address.rs index 9699e304..ea0c3b7f 100644 --- a/indexer/tasks/src/multiera/multiera_address.rs +++ b/indexer/tasks/src/multiera/multiera_address.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; -use cml_chain::certs::Credential; +use cml_chain::address::StakeCredentialEncoding; +use cml_chain::certs::{Credential, StakeCredential}; use cml_chain::{ address::{BaseAddress, EnterpriseAddress, PointerAddress, RewardAddress}, byron::ByronAddress, @@ -163,6 +164,15 @@ fn queue_certificate( TxCredentialRelationValue::StakeRegistration, ); } + MultiEraCertificate::RegCert(registration) => { + let credential = registration.stake_credential.to_cbor_bytes(); + + vkey_relation_map.add_relation( + tx_id, + &credential, + TxCredentialRelationValue::StakeRegistration, + ); + } MultiEraCertificate::StakeDeregistration(deregistration) => { let credential = deregistration.stake_credential.to_cbor_bytes(); @@ -172,8 +182,18 @@ fn queue_certificate( TxCredentialRelationValue::StakeDeregistration, ); } + MultiEraCertificate::UnregCert(deregistration) => { + let credential = deregistration.stake_credential.to_cbor_bytes(); + + vkey_relation_map.add_relation( + tx_id, + &credential, + TxCredentialRelationValue::StakeDeregistration, + ); + } MultiEraCertificate::PoolRegistration(registration) => { - let operator_credential = registration.pool_params.operator.to_raw_bytes().to_vec(); + let operator_credential = + StakeCredential::new_pub_key(registration.pool_params.operator).to_cbor_bytes(); vkey_relation_map.add_relation( tx_id, @@ -195,7 +215,7 @@ fn queue_certificate( ); for &owner in registration.pool_params.pool_owners.iter() { - let owner_credential = owner.to_raw_bytes().to_vec(); + let owner_credential = StakeCredential::new_pub_key(owner).to_cbor_bytes(); vkey_relation_map.add_relation( tx_id, @@ -205,7 +225,7 @@ fn queue_certificate( } } MultiEraCertificate::PoolRetirement(retirement) => { - let operator_credential = retirement.pool.to_raw_bytes().to_vec(); + let operator_credential = StakeCredential::new_pub_key(retirement.pool).to_cbor_bytes(); vkey_relation_map.add_relation( tx_id, &operator_credential, @@ -232,18 +252,90 @@ fn queue_certificate( } } } - MultiEraCertificate::RegCert(_) => {} - MultiEraCertificate::UnregCert(_) => {} - MultiEraCertificate::VoteDelegCert(_) => {} - MultiEraCertificate::StakeVoteDelegCert(_) => {} - MultiEraCertificate::StakeRegDelegCert(_) => {} - MultiEraCertificate::VoteRegDelegCert(_) => {} - MultiEraCertificate::StakeVoteRegDelegCert(_) => {} + MultiEraCertificate::VoteDelegCert(cert) => { + let voter_credential = cert.stake_credential.to_cbor_bytes(); + + vkey_relation_map.add_relation( + tx_id, + &voter_credential, + TxCredentialRelationValue::DrepStakeDelegation, + ); + + let drep_cred = drep_to_credential(&cert.d_rep); + + if let Some(drep_cred) = drep_cred { + vkey_relation_map.add_relation( + tx_id, + &drep_cred.to_cbor_bytes(), + TxCredentialRelationValue::DrepStakeDelegationTarget, + ); + } + } + MultiEraCertificate::StakeVoteDelegCert(cert) => { + let credential = cert.stake_credential.to_cbor_bytes(); + + delegate_to_pool(vkey_relation_map, tx_id, &credential, cert.pool); + delegate_to_drep(vkey_relation_map, tx_id, &credential, &cert.d_rep); + } + MultiEraCertificate::StakeRegDelegCert(cert) => { + let credential = cert.stake_credential.to_cbor_bytes(); + + vkey_relation_map.add_relation( + tx_id, + &credential, + TxCredentialRelationValue::StakeRegistration, + ); + + delegate_to_pool(vkey_relation_map, tx_id, &credential, cert.pool); + } + MultiEraCertificate::VoteRegDelegCert(cert) => { + let credential = cert.stake_credential.to_cbor_bytes(); + + vkey_relation_map.add_relation( + tx_id, + &credential, + TxCredentialRelationValue::StakeRegistration, + ); + + delegate_to_drep(vkey_relation_map, tx_id, &credential, &cert.d_rep); + } + MultiEraCertificate::StakeVoteRegDelegCert(cert) => { + let credential = cert.stake_credential.to_cbor_bytes(); + vkey_relation_map.add_relation( + tx_id, + &credential, + TxCredentialRelationValue::StakeRegistration, + ); + + delegate_to_pool(vkey_relation_map, tx_id, &credential, cert.pool); + delegate_to_drep(vkey_relation_map, tx_id, &credential, &cert.d_rep); + } MultiEraCertificate::AuthCommitteeHotCert(_) => {} MultiEraCertificate::ResignCommitteeColdCert(_) => {} - MultiEraCertificate::RegDrepCert(_) => {} - MultiEraCertificate::UnregDrepCert(_) => {} - MultiEraCertificate::UpdateDrepCert(_) => {} + MultiEraCertificate::RegDrepCert(cert) => { + let operator_credential = cert.drep_credential.to_cbor_bytes(); + vkey_relation_map.add_relation( + tx_id, + &operator_credential, + TxCredentialRelationValue::DrepOperation, + ); + } + MultiEraCertificate::UnregDrepCert(cert) => { + let operator_credential = cert.drep_credential.to_cbor_bytes(); + vkey_relation_map.add_relation( + tx_id, + &operator_credential, + TxCredentialRelationValue::DrepOperation, + ); + } + MultiEraCertificate::UpdateDrepCert(cert) => { + let operator_credential = cert.drep_credential.to_cbor_bytes(); + vkey_relation_map.add_relation( + tx_id, + &operator_credential, + TxCredentialRelationValue::DrepOperation, + ); + } }; } @@ -368,3 +460,58 @@ fn queue_address_credential( address_relation, }); } + +// AlwaysAbstain and AlwaysNoConfidence are ignored here because we only can add +// relations to actual credentials +pub fn drep_to_credential(d_rep: &cml_chain::certs::DRep) -> Option { + match d_rep { + cml_chain::certs::DRep::Key { pool, .. } => Some(StakeCredential::new_pub_key(*pool)), + cml_chain::certs::DRep::Script { script_hash, .. } => { + Some(StakeCredential::new_script(*script_hash)) + } + cml_chain::certs::DRep::AlwaysAbstain { .. } => None, + cml_chain::certs::DRep::AlwaysNoConfidence { .. } => None, + } +} + +fn delegate_to_drep( + vkey_relation_map: &mut RelationMap, + tx_id: i64, + credential: &[u8], + d_rep: &cml_chain::certs::DRep, +) { + vkey_relation_map.add_relation( + tx_id, + credential, + TxCredentialRelationValue::DrepStakeDelegation, + ); + + let drep_cred = drep_to_credential(d_rep); + + if let Some(drep_cred) = drep_cred { + vkey_relation_map.add_relation( + tx_id, + &drep_cred.to_cbor_bytes(), + TxCredentialRelationValue::DrepStakeDelegationTarget, + ); + } +} + +fn delegate_to_pool( + vkey_relation_map: &mut RelationMap, + tx_id: i64, + credential: &[u8], + pool: cml_crypto::Ed25519KeyHash, +) { + vkey_relation_map.add_relation( + tx_id, + credential, + TxCredentialRelationValue::StakeDelegation, + ); + + vkey_relation_map.add_relation( + tx_id, + &StakeCredential::new_pub_key(pool).to_cbor_bytes(), + TxCredentialRelationValue::DelegationTarget, + ); +} diff --git a/indexer/tasks/src/multiera/multiera_address_delegation.rs b/indexer/tasks/src/multiera/multiera_address_delegation.rs index 6586c426..b0e9ece2 100644 --- a/indexer/tasks/src/multiera/multiera_address_delegation.rs +++ b/indexer/tasks/src/multiera/multiera_address_delegation.rs @@ -62,9 +62,19 @@ async fn handle( MultiEraCertificate::StakeDelegation(delegation) => { (delegation.stake_credential.clone(), Some(delegation.pool)) } + MultiEraCertificate::StakeVoteDelegCert(cert) => { + (cert.stake_credential.clone(), Some(cert.pool)) + } + MultiEraCertificate::StakeRegDelegCert(cert) => { + (cert.stake_credential.clone(), Some(cert.pool)) + } + MultiEraCertificate::StakeVoteRegDelegCert(cert) => { + (cert.stake_credential.clone(), Some(cert.pool)) + } MultiEraCertificate::StakeDeregistration(deregistration) => { (deregistration.stake_credential.clone(), None) } + MultiEraCertificate::UnregCert(unreg) => (unreg.stake_credential.clone(), None), _ => continue, }; diff --git a/indexer/tasks/src/multiera/multiera_asset_utxo.rs b/indexer/tasks/src/multiera/multiera_asset_utxo.rs index 36e5eaec..8e718c8c 100644 --- a/indexer/tasks/src/multiera/multiera_asset_utxo.rs +++ b/indexer/tasks/src/multiera/multiera_asset_utxo.rs @@ -72,11 +72,14 @@ async fn handle( for (tx_body, cardano_transaction) in block.1.transaction_bodies().iter().zip(multiera_txs) { let collateral_inputs = tx_body .collateral_inputs() - .cloned() - .unwrap_or_default() - .into_iter() - .map(MultiEraTransactionInput::Shelley) - .collect::>(); + .map(|collateral_inputs| { + collateral_inputs + .iter() + .cloned() + .map(MultiEraTransactionInput::Shelley) + .collect::>() + }) + .unwrap_or_else(std::vec::Vec::new); for input in tx_body.inputs().iter().chain(collateral_inputs.iter()) { let utxo = multiera_used_inputs_to_outputs_map diff --git a/indexer/tasks/src/multiera/multiera_datum.rs b/indexer/tasks/src/multiera/multiera_datum.rs index 21acf81c..624e70b3 100644 --- a/indexer/tasks/src/multiera/multiera_datum.rs +++ b/indexer/tasks/src/multiera/multiera_datum.rs @@ -65,19 +65,16 @@ async fn handle_datum( .zip(block.1.transaction_witness_sets().iter()) .zip(multiera_txs) { - for datum in tx_witness_set - .plutus_datums - .clone() - .unwrap_or_default() - .iter() - { - let hash = datum.hash(); - hash_to_tx - .entry(hash) - .or_insert_with(|| cardano_transaction.id); - hash_to_data - .entry(hash) - .or_insert_with(|| datum.to_cbor_bytes()); + if let Some(plutus_datums) = &tx_witness_set.plutus_datums { + for datum in plutus_datums.iter() { + let hash = datum.hash(); + hash_to_tx + .entry(hash) + .or_insert_with(|| cardano_transaction.id); + hash_to_data + .entry(hash) + .or_insert_with(|| datum.to_cbor_bytes()); + } } for output in tx_body.outputs().iter() { let output = match output { diff --git a/indexer/tasks/src/multiera/multiera_drep_delegation.rs b/indexer/tasks/src/multiera/multiera_drep_delegation.rs new file mode 100644 index 00000000..c9b27bd4 --- /dev/null +++ b/indexer/tasks/src/multiera/multiera_drep_delegation.rs @@ -0,0 +1,132 @@ +use crate::{ + multiera::multiera_stake_credentials::MultieraStakeCredentialTask, + types::{AddressCredentialRelationValue, TxCredentialRelationValue}, +}; +use cml_core::serialization::Serialize; +use cml_crypto::RawBytesEncoding; +use cml_multi_era::utils::MultiEraCertificate; +use entity::{ + prelude::*, + sea_orm::{prelude::*, DatabaseTransaction}, +}; +use sea_orm::{Order, QueryOrder, Set}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Deref; + +use super::{ + multiera_address::drep_to_credential, + multiera_address_credential_relations::QueuedAddressCredentialRelation, + multiera_txs::MultieraTransactionTask, relation_map::RelationMap, +}; +use crate::config::EmptyConfig::EmptyConfig; +use crate::dsl::database_task::BlockGlobalInfo; +use crate::dsl::task_macro::*; + +carp_task! { + name MultieraDrepDelegationTask; + configuration EmptyConfig; + doc "Tracks stake delegation actions to dreps"; + era multiera; + dependencies [MultieraStakeCredentialTask]; + read [multiera_txs, multiera_stake_credential]; + write []; + should_add_task |block, _properties| { + block + .1 + .transaction_bodies() + .iter() + .any(|x| x.certs().is_some()); + }; + execute |previous_data, task| handle( + task.db_tx, + task.block, + &previous_data.multiera_txs, + &previous_data.multiera_stake_credential, + ); + merge_result |_previous_data, _result| {}; +} + +async fn handle( + db_tx: &DatabaseTransaction, + block: BlockInfo<'_, cml_multi_era::MultiEraBlock, BlockGlobalInfo>, + multiera_txs: &[TransactionModel], + multiera_stake_credential: &BTreeMap, StakeCredentialModel>, +) -> Result<(), DbErr> { + for (tx_body, cardano_transaction) in block.1.transaction_bodies().iter().zip(multiera_txs) { + let certs = match tx_body.certs() { + None => continue, + Some(certs) => certs, + }; + for cert in certs { + { + let tx_id = cardano_transaction.id; + let cert = &cert; + let (credential, drep) = match cert { + MultiEraCertificate::VoteDelegCert(delegation) => ( + delegation.stake_credential.clone(), + drep_to_credential(&delegation.d_rep), + ), + MultiEraCertificate::StakeVoteDelegCert(delegation) => ( + delegation.stake_credential.clone(), + drep_to_credential(&delegation.d_rep), + ), + MultiEraCertificate::VoteRegDelegCert(delegation) => ( + delegation.stake_credential.clone(), + drep_to_credential(&delegation.d_rep), + ), + MultiEraCertificate::StakeVoteRegDelegCert(delegation) => ( + delegation.stake_credential.clone(), + drep_to_credential(&delegation.d_rep), + ), + MultiEraCertificate::StakeDeregistration(deregistration) => { + (deregistration.stake_credential.clone(), None) + } + MultiEraCertificate::UnregCert(unreg) => (unreg.stake_credential.clone(), None), + _ => continue, + }; + + let credential = credential.to_cbor_bytes(); + let drep = drep.map(|cred| cred.to_cbor_bytes()); + + let stake_credential_id = multiera_stake_credential + .get(&credential.to_vec()) + .unwrap() + .id; + + let previous_entry = entity::stake_delegation_drep::Entity::find() + .filter( + entity::stake_delegation_drep::Column::StakeCredential + .eq(stake_credential_id), + ) + .order_by_desc(entity::stake_delegation_drep::Column::Id) + .one(db_tx) + .await?; + + if let Some((previous, drep)) = previous_entry + .as_ref() + .and_then(|entry| entry.drep_credential.as_ref()) + .zip(drep.as_ref()) + { + // re-delegating shouldn't have any effect. + if previous == drep { + continue; + } + } + + entity::stake_delegation_drep::ActiveModel { + stake_credential: Set(stake_credential_id), + drep_credential: Set(drep), + tx_id: Set(tx_id), + previous_drep_credential: Set( + previous_entry.and_then(|entity| entity.drep_credential) + ), + ..Default::default() + } + .save(db_tx) + .await?; + }; + } + } + + Ok(()) +} diff --git a/indexer/tasks/src/multiera/multiera_governance_voting.rs b/indexer/tasks/src/multiera/multiera_governance_voting.rs new file mode 100644 index 00000000..2302c201 --- /dev/null +++ b/indexer/tasks/src/multiera/multiera_governance_voting.rs @@ -0,0 +1,59 @@ +use crate::config::EmptyConfig::EmptyConfig; +use crate::{dsl::task_macro::*, multiera::multiera_txs::MultieraTransactionTask}; +use cml_crypto::Serialize; +use entity::governance_votes::{ActiveModel, Model}; +use sea_orm::{prelude::*, Set}; + +carp_task! { + name MultieraGovernanceVotingTask; + configuration EmptyConfig; + doc ""; + era multiera; + dependencies [MultieraTransactionTask]; + read [multiera_txs]; + write []; + should_add_task |block, _properties| { + block.1.transaction_bodies().iter().any(|x| x.voting_procedures().is_some()) + }; + execute |previous_data, task| handle( + task.db_tx, + task.block, + &previous_data.multiera_txs, + ); + merge_result |_previous_data, _result| {}; +} + +async fn handle( + db_tx: &DatabaseTransaction, + block: BlockInfo<'_, cml_multi_era::MultiEraBlock, BlockGlobalInfo>, + multiera_txs: &[TransactionModel], +) -> Result<(), DbErr> { + let mut queued_inserts = vec![]; + for (tx_body, cardano_transaction) in block.1.transaction_bodies().iter().zip(multiera_txs) { + let voting_procedures = if let Some(voting_procedures) = tx_body.voting_procedures() { + voting_procedures + } else { + continue; + }; + + for (voter, gov_action_id) in voting_procedures.iter() { + for (gov_action_id, vote) in gov_action_id.iter() { + queued_inserts.push(ActiveModel { + tx_id: Set(cardano_transaction.id), + voter: Set(voter.to_cbor_bytes()), + gov_action_id: Set(gov_action_id.to_cbor_bytes()), + vote: Set(vote.to_cbor_bytes()), + ..Default::default() + }) + } + } + } + + if !queued_inserts.is_empty() { + GovernanceVote::insert_many(queued_inserts.into_iter()) + .exec(db_tx) + .await?; + } + + Ok(()) +} diff --git a/indexer/tasks/src/multiera/multiera_projected_nft.rs b/indexer/tasks/src/multiera/multiera_projected_nft.rs index c8f91686..f2cb00cf 100644 --- a/indexer/tasks/src/multiera/multiera_projected_nft.rs +++ b/indexer/tasks/src/multiera/multiera_projected_nft.rs @@ -1,5 +1,5 @@ use cardano_projected_nft::{Owner, Redeem, State, Status}; -use cml_chain::plutus::{Redeemer, RedeemerTag}; +use cml_chain::plutus::{LegacyRedeemer, PlutusData, RedeemerTag, Redeemers}; use cml_chain::transaction::DatumOption; use cml_core::serialization::{FromBytes, Serialize}; use cml_crypto::{Ed25519KeyHash, RawBytesEncoding, TransactionHash}; @@ -705,23 +705,44 @@ fn extract_operation_and_datum( Ok(result) } -fn get_projected_nft_redeemers(redeemers: &[Redeemer]) -> Result, DbErr> { - let mut result = BTreeMap::new(); +fn get_projected_nft_redeemers(redeemers: &Redeemers) -> Result, DbErr> { + fn build_map<'a>( + redeemers: impl Iterator, + ) -> BTreeMap { + let mut result = BTreeMap::new(); - for redeemer in redeemers { - if redeemer.tag != RedeemerTag::Spend { - continue; - } - - match Redeem::try_from(redeemer.data.to_cbor_bytes().as_slice()) { - Ok(redeem) => { - result.insert(redeemer.index as i64, redeem); + for (tag, index, data) in redeemers { + if tag != RedeemerTag::Spend { + continue; } - Err(err) => { - tracing::info!("Can't parse redeemer: {err}"); + + match Redeem::try_from(data.to_cbor_bytes().as_slice()) { + Ok(redeem) => { + result.insert(index as i64, redeem); + } + Err(err) => { + tracing::info!("Can't parse redeemer: {err}"); + } } } + + result } - Ok(result) + match redeemers { + Redeemers::ArrLegacyRedeemer { + arr_legacy_redeemer, + arr_legacy_redeemer_encoding: _, + } => Ok(build_map(arr_legacy_redeemer.iter().map(|redeemeer| { + (redeemeer.tag, redeemeer.index, &redeemeer.data) + }))), + Redeemers::MapRedeemerKeyToRedeemerVal { + map_redeemer_key_to_redeemer_val, + map_redeemer_key_to_redeemer_val_encoding: _, + } => Ok(build_map( + map_redeemer_key_to_redeemer_val + .iter() + .map(|(key, val)| (key.tag, key.index, &val.data)), + )), + } } diff --git a/indexer/tasks/src/multiera/multiera_reference_inputs.rs b/indexer/tasks/src/multiera/multiera_reference_inputs.rs index 33b5faed..1b965fa3 100644 --- a/indexer/tasks/src/multiera/multiera_reference_inputs.rs +++ b/indexer/tasks/src/multiera/multiera_reference_inputs.rs @@ -31,7 +31,11 @@ carp_task! { read [multiera_txs]; write [vkey_relation_map]; should_add_task |block, _properties| { - block.1.transaction_bodies().iter().any(|tx| !tx.reference_inputs().cloned().unwrap_or_default().is_empty()) + block.1.transaction_bodies().iter().any(|tx| { + !tx.reference_inputs() + .map(|reference_inputs| reference_inputs.is_empty()) + .unwrap_or(true) + }) }; execute |previous_data, task| handle_input( task.db_tx, @@ -62,11 +66,14 @@ async fn handle_input( for (tx_body, cardano_transaction) in txs.iter().zip(multiera_txs) { let refs = tx_body .reference_inputs() - .cloned() - .unwrap_or_default() - .into_iter() - .map(MultiEraTransactionInput::Shelley) - .collect(); + .map(|reference_inputs| { + reference_inputs + .iter() + .cloned() + .map(MultiEraTransactionInput::Shelley) + .collect::>() + }) + .unwrap_or_else(std::vec::Vec::new); queued_inputs.push((refs, cardano_transaction.id)); } diff --git a/indexer/tasks/src/multiera/multiera_stake_credentials.rs b/indexer/tasks/src/multiera/multiera_stake_credentials.rs index 2f7b5c22..a5fc6e96 100644 --- a/indexer/tasks/src/multiera/multiera_stake_credentials.rs +++ b/indexer/tasks/src/multiera/multiera_stake_credentials.rs @@ -63,8 +63,14 @@ async fn handle_stake_credentials( tx_witness.clone(), ); - for signer in tx_body.required_signers().cloned().unwrap_or_default() { - let owner_credential = cml_chain::certs::Credential::new_pub_key(signer) + let required_signers = if let Some(required_signers) = tx_body.required_signers() { + required_signers + } else { + continue; + }; + + for signer in required_signers { + let owner_credential = cml_chain::certs::Credential::new_pub_key(*signer) .to_raw_bytes() .to_vec(); vkey_relation_map.add_relation( @@ -149,7 +155,7 @@ fn queue_witness( witness_set: TransactionWitnessSet, ) { if let Some(vkeys) = witness_set.vkeywitnesses { - for vkey in vkeys { + for vkey in vkeys.iter() { vkey_relation_map.add_relation( tx_id, vkey.vkey.hash().to_raw_bytes(), @@ -158,14 +164,14 @@ fn queue_witness( } } if let Some(scripts) = witness_set.native_scripts { - for script in scripts { + for script in scripts.iter() { vkey_relation_map.add_relation( tx_id, script.hash().to_raw_bytes(), TxCredentialRelationValue::Witness, ); - let vkeys_in_script = RequiredSignersSet::from(&script); + let vkeys_in_script = RequiredSignersSet::from(script); for vkey_in_script in vkeys_in_script { vkey_relation_map.add_relation( tx_id, @@ -177,7 +183,7 @@ fn queue_witness( } if let Some(scripts) = &witness_set.plutus_v1_scripts { - for script in scripts { + for script in scripts.iter() { vkey_relation_map.add_relation( tx_id, script.hash().to_raw_bytes(), @@ -186,7 +192,7 @@ fn queue_witness( } } if let Some(scripts) = &witness_set.plutus_v2_scripts { - for script in scripts { + for script in scripts.iter() { vkey_relation_map.add_relation( tx_id, script.hash().to_raw_bytes(), diff --git a/indexer/tasks/src/multiera/multiera_txs.rs b/indexer/tasks/src/multiera/multiera_txs.rs index 3d6f2374..13c79be5 100644 --- a/indexer/tasks/src/multiera/multiera_txs.rs +++ b/indexer/tasks/src/multiera/multiera_txs.rs @@ -23,7 +23,7 @@ carp_task! { execute |previous_data, task| handle_tx( task.db_tx, task.block, - &previous_data.multiera_block.as_ref().unwrap(), + previous_data.multiera_block.as_ref().unwrap(), task.config.readonly, task.config.include_payload ); diff --git a/indexer/tasks/src/multiera/multiera_unused_input.rs b/indexer/tasks/src/multiera/multiera_unused_input.rs index 9bddc303..75fb873b 100644 --- a/indexer/tasks/src/multiera/multiera_unused_input.rs +++ b/indexer/tasks/src/multiera/multiera_unused_input.rs @@ -22,7 +22,11 @@ carp_task! { write [vkey_relation_map]; should_add_task |block, _properties| { // if any txs has collateral defined, then it has some unused input (either collateral or main inputs if tx failed) - block.1.transaction_bodies().iter().any(|tx| !tx.collateral_inputs().cloned().unwrap_or_default().is_empty()) + block.1.transaction_bodies().iter().any(|tx| { + !tx.collateral_inputs() + .map(|collateral_inputs| collateral_inputs.is_empty()) + .unwrap_or(true) + }) }; execute |previous_data, task| handle_unused_input( task.db_tx, @@ -56,11 +60,14 @@ async fn handle_unused_input( // you can use the is_valid field to know what kind of input it actually is let refs = tx_body .collateral_inputs() - .cloned() - .unwrap_or_default() - .into_iter() - .map(MultiEraTransactionInput::Shelley) - .collect(); + .map(|collateral_inputs| { + collateral_inputs + .iter() + .cloned() + .map(MultiEraTransactionInput::Shelley) + .collect() + }) + .unwrap_or_else(std::vec::Vec::new); queued_unused_inputs.push((refs, cardano_transaction.id)) } } diff --git a/indexer/tasks/src/multiera/multiera_used_inputs.rs b/indexer/tasks/src/multiera/multiera_used_inputs.rs index 83a3fe43..29b96d7b 100644 --- a/indexer/tasks/src/multiera/multiera_used_inputs.rs +++ b/indexer/tasks/src/multiera/multiera_used_inputs.rs @@ -75,11 +75,14 @@ async fn handle_input( if !cardano_transaction.is_valid { let refs = tx_body .collateral_inputs() - .cloned() - .unwrap_or_default() - .into_iter() - .map(MultiEraTransactionInput::Shelley) - .collect(); + .map(|collateral_inputs| { + collateral_inputs + .iter() + .cloned() + .map(MultiEraTransactionInput::Shelley) + .collect() + }) + .unwrap_or_else(std::vec::Vec::new); queued_inputs.push((refs, cardano_transaction.id)) } } diff --git a/indexer/tasks/src/multiera/utils/common.rs b/indexer/tasks/src/multiera/utils/common.rs index 59a7c566..5ee62aa5 100644 --- a/indexer/tasks/src/multiera/utils/common.rs +++ b/indexer/tasks/src/multiera/utils/common.rs @@ -1,5 +1,6 @@ use cml_chain::certs::Credential; use cml_chain::transaction::DatumOption; +use cml_chain::NonemptySetPlutusData; use cml_core::serialization::{Deserialize, Serialize, ToBytes}; use cml_crypto::RawBytesEncoding; use cml_multi_era::utils::{MultiEraTransactionInput, MultiEraTransactionOutput}; @@ -57,7 +58,7 @@ pub fn get_asset_amount( pub fn get_plutus_datum_for_output( output: &cml_multi_era::utils::MultiEraTransactionOutput, - plutus_data: &[cml_chain::plutus::PlutusData], + plutus_data: &Option, ) -> Option { let output = match output { MultiEraTransactionOutput::Byron(_) => { @@ -75,10 +76,12 @@ pub fn get_plutus_datum_for_output( match datum_option { DatumOption::Datum { datum, .. } => Some(datum), - DatumOption::Hash { datum_hash, .. } => plutus_data - .iter() - .find(|datum| datum.hash() == datum_hash) - .cloned(), + DatumOption::Hash { datum_hash, .. } => plutus_data.as_ref().and_then(|non_empty_set| { + non_empty_set + .iter() + .find(|datum| datum.hash() == datum_hash) + .cloned() + }), } } diff --git a/indexer/tasks/src/types.rs b/indexer/tasks/src/types.rs index f2acf9ef..cc48e22b 100644 --- a/indexer/tasks/src/types.rs +++ b/indexer/tasks/src/types.rs @@ -22,6 +22,9 @@ pub enum TxCredentialRelationValue { Withdrawal, RequiredSigner, InNativeScript, // keyhash in scripts including mints + DrepStakeDelegation, + DrepStakeDelegationTarget, + DrepOperation, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -56,6 +59,9 @@ impl From for i32 { TxCredentialRelationValue::UnusedOutputStake => 0b10000000000000000000, TxCredentialRelationValue::ReferenceInput => 0b100000000000000000000, TxCredentialRelationValue::ReferenceInputStake => 0b1000000000000000000000, + TxCredentialRelationValue::DrepStakeDelegation => 0b10000000000000000000000, + TxCredentialRelationValue::DrepStakeDelegationTarget => 0b100000000000000000000000, + TxCredentialRelationValue::DrepOperation => 0b1000000000000000000000000, } } } diff --git a/webserver/server/app/controllers/DrepDelegationForAddressController.ts b/webserver/server/app/controllers/DrepDelegationForAddressController.ts new file mode 100644 index 00000000..e319daf3 --- /dev/null +++ b/webserver/server/app/controllers/DrepDelegationForAddressController.ts @@ -0,0 +1,72 @@ +import { Body, Controller, TsoaResponse, Res, Post, Route, SuccessResponse } from 'tsoa'; +import { StatusCodes } from 'http-status-codes'; +import tx from 'pg-tx'; +import pool from '../services/PgPoolSingleton'; +import type { ErrorShape } from '../../../shared/errors'; +import { genErrorMessage } from '../../../shared/errors'; +import { Errors } from '../../../shared/errors'; +import type { EndpointTypes } from '../../../shared/routes'; +import { Routes } from '../../../shared/routes'; +import { Address, RewardAddress } from '@dcspark/cardano-multiplatform-lib-nodejs'; +import { DrepDelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; +import { drepDelegationForAddress } from '../services/DrepDelegationForAddress'; + +const route = Routes.drepDelegationForAddress; + +@Route('delegation/drep/address') +export class DrepDelegationForAddressController extends Controller { + /** + * Returns the drep of the last delegation for this address. + */ + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async drepDelegationForAddress( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + const address = Address.from_bech32(requestBody.address); + const rewardAddr = RewardAddress.from_address(address); + const stakingCred = address.staking_cred(); + + let credential: Buffer; + + if(rewardAddr) { + credential = Buffer.from(rewardAddr.payment().to_cbor_bytes()); + } + else if(stakingCred) { + credential = Buffer.from(stakingCred.to_cbor_bytes()); + } + else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.UNPROCESSABLE_ENTITY, + genErrorMessage(Errors.IncorrectAddressFormat, { + addresses: [requestBody.address], + }) + ); + } + + const response = await tx< + DrepDelegationForAddressResponse + >(pool, async dbTx => { + const data = await drepDelegationForAddress({ + address: credential, + until: requestBody.until, + dbTx + }); + + return { + drep: data ? data.drep : null, + txId: data ? data.tx_id : null, + } + }); + + return response; + } +} + diff --git a/webserver/server/app/controllers/GovernanceVotesByActionIdsController.ts b/webserver/server/app/controllers/GovernanceVotesByActionIdsController.ts new file mode 100644 index 00000000..19a4b773 --- /dev/null +++ b/webserver/server/app/controllers/GovernanceVotesByActionIdsController.ts @@ -0,0 +1,88 @@ +import { Body, Controller, TsoaResponse, Res, Post, Route, SuccessResponse } from 'tsoa'; +import { StatusCodes } from 'http-status-codes'; +import tx from 'pg-tx'; +import pool from '../services/PgPoolSingleton'; +import type { ErrorShape } from '../../../shared/errors'; +import { genErrorMessage } from '../../../shared/errors'; +import { Errors } from '../../../shared/errors'; +import type { EndpointTypes } from '../../../shared/routes'; +import { Routes } from '../../../shared/routes'; +import { resolveUntilTransaction } from '../services/PaginationService'; +import { expectType } from 'tsd'; +import { governanceCredentialDidVote } from '../services/GovernanceCredentialVotesByActionIds'; +import { GOVERNANCE_VOTES_BY_GOV_IDS_LIMIT } from '../../../shared/constants'; + +const route = Routes.governanceCredentialVotesByGovActionId; + +@Route('governance/credential/votesByGovId') +export class GovernanceCredentialVotesByGovId extends Controller { + /** + * Gets votes cast for a set of governance action ids. + */ + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async governanceCredentialDidVote( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + + if (requestBody.actionIds.length > GOVERNANCE_VOTES_BY_GOV_IDS_LIMIT.MAX_ACTION_IDS) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.GovActionIdsLimitExceeded, { + limit: GOVERNANCE_VOTES_BY_GOV_IDS_LIMIT.MAX_ACTION_IDS, + found: requestBody.actionIds.length, + }) + ); + } + + let credential = Buffer.from(requestBody.credential, 'hex'); + + const response = await tx( + pool, + async dbTx => { + const until = await resolveUntilTransaction({ + block_hash: Buffer.from(requestBody.untilBlock, 'hex'), + dbTx, + }); + + if (until == null) { + return genErrorMessage(Errors.BlockHashNotFound, { + untilBlock: requestBody.untilBlock, + }); + } + + if(requestBody.actionIds.length === 0) { + return []; + } + + const data = await governanceCredentialDidVote({ + credential, + govActionIds: requestBody.actionIds.map(actionId => Buffer.from(actionId, 'hex')), + until: until.tx_id, + dbTx, + }); + + return data.map(vote => ({ + actionId: vote.govActionId.toString('hex'), + txId: vote.txId, + payload: vote.vote.toString('hex'), + })); + } + ); + + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); + } + + return response; + } +} diff --git a/webserver/server/app/controllers/GovernanceVotesForCredentialController.ts b/webserver/server/app/controllers/GovernanceVotesForCredentialController.ts new file mode 100644 index 00000000..0009aca9 --- /dev/null +++ b/webserver/server/app/controllers/GovernanceVotesForCredentialController.ts @@ -0,0 +1,83 @@ +import { Body, Controller, TsoaResponse, Res, Post, Route, SuccessResponse } from 'tsoa'; +import { StatusCodes } from 'http-status-codes'; +import tx from 'pg-tx'; +import pool from '../services/PgPoolSingleton'; +import type { ErrorShape } from '../../../shared/errors'; +import { genErrorMessage } from '../../../shared/errors'; +import { Errors } from '../../../shared/errors'; +import type { EndpointTypes } from '../../../shared/routes'; +import { Routes } from '../../../shared/routes'; +import { governanceVotesForAddress } from '../services/GovernanceVotesForAddress'; +import { resolvePageStart, resolveUntilTransaction } from '../services/PaginationService'; +import { GOVERNANCE_VOTES_LIMIT } from '../../../shared/constants'; +import { expectType } from 'tsd'; +import { GovernanceVotesForCredentialResponse } from '../../../shared/models/Governance'; + +const route = Routes.governanceVotesForCredential; + +@Route('governance/credential/votes') +export class GovernanceVotesForCredential extends Controller { + /** + * Returns votes cast by a credential. Sorted in descending order (newer first). + */ + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async governanceVotesForAddress( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + let credential = Buffer.from(requestBody.credential, 'hex'); + + const response = await tx( + pool, + async dbTx => { + const [until, pageStart] = await Promise.all([ + resolveUntilTransaction({ block_hash: Buffer.from(requestBody.untilBlock, 'hex'), dbTx }), + requestBody.after == null + ? Promise.resolve(undefined) + : resolvePageStart({ + after_block: Buffer.from(requestBody.after.block, 'hex'), + after_tx: Buffer.from(requestBody.after.tx, 'hex'), + dbTx, + }), + ]); + + if (until == null) { + return genErrorMessage(Errors.BlockHashNotFound, { + untilBlock: requestBody.untilBlock, + }); + } + + if (requestBody.after && !pageStart) { + return genErrorMessage(Errors.PageStartNotFound, { + blockHash: requestBody.after.block, + txHash: requestBody.after.tx, + }); + } + + const data = await governanceVotesForAddress({ + credential, + before: pageStart?.tx_id || Number.MAX_SAFE_INTEGER, + until: until.tx_id, + limit: requestBody.limit || GOVERNANCE_VOTES_LIMIT.DEFAULT_PAGE_SIZE, + dbTx, + }); + + return data as GovernanceVotesForCredentialResponse; + } + ); + + if ('code' in response) { + expectType>(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse(StatusCodes.CONFLICT, response); + } + + return response; + } +} diff --git a/webserver/server/app/models/delegation/drepDelegationForAddress.queries.ts b/webserver/server/app/models/delegation/drepDelegationForAddress.queries.ts new file mode 100644 index 00000000..2d1b12c2 --- /dev/null +++ b/webserver/server/app/models/delegation/drepDelegationForAddress.queries.ts @@ -0,0 +1,41 @@ +/** Types generated for queries found in "app/models/delegation/drepDelegationForAddress.sql" */ +import { PreparedQuery } from '@pgtyped/runtime'; + +/** 'SqlDrepStakeDelegationForAddress' parameters type */ +export interface ISqlDrepStakeDelegationForAddressParams { + credential: Buffer; + slot: number; +} + +/** 'SqlDrepStakeDelegationForAddress' return type */ +export interface ISqlDrepStakeDelegationForAddressResult { + drep: string; + tx_id: string; +} + +/** 'SqlDrepStakeDelegationForAddress' query type */ +export interface ISqlDrepStakeDelegationForAddressQuery { + params: ISqlDrepStakeDelegationForAddressParams; + result: ISqlDrepStakeDelegationForAddressResult; +} + +const sqlDrepStakeDelegationForAddressIR: any = {"usedParamSet":{"credential":true,"slot":true},"params":[{"name":"credential","required":true,"transform":{"type":"scalar"},"locs":[{"a":384,"b":395}]},{"name":"slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":418,"b":423}]}],"statement":"SELECT encode(drep_credential, 'hex') as \"drep!\", encode(\"Transaction\".hash, 'hex') as \"tx_id!\"\nFROM \"StakeDelegationDrepCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationDrepCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE\n\t\"StakeCredential\".credential = :credential! AND\n\t\"Block\".slot <= :slot!\nORDER BY \"Transaction\".id DESC\nLIMIT 1"}; + +/** + * Query generated from SQL: + * ``` + * SELECT encode(drep_credential, 'hex') as "drep!", encode("Transaction".hash, 'hex') as "tx_id!" + * FROM "StakeDelegationDrepCredentialRelation" + * JOIN "StakeCredential" ON stake_credential = "StakeCredential".id + * JOIN "Transaction" ON "Transaction".id = "StakeDelegationDrepCredentialRelation".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * "StakeCredential".credential = :credential! AND + * "Block".slot <= :slot! + * ORDER BY "Transaction".id DESC + * LIMIT 1 + * ``` + */ +export const sqlDrepStakeDelegationForAddress = new PreparedQuery(sqlDrepStakeDelegationForAddressIR); + + diff --git a/webserver/server/app/models/delegation/drepDelegationForAddress.sql b/webserver/server/app/models/delegation/drepDelegationForAddress.sql new file mode 100644 index 00000000..5982f293 --- /dev/null +++ b/webserver/server/app/models/delegation/drepDelegationForAddress.sql @@ -0,0 +1,11 @@ +/* @name sqlDrepStakeDelegationForAddress */ +SELECT encode(drep_credential, 'hex') as "drep!", encode("Transaction".hash, 'hex') as "tx_id!" +FROM "StakeDelegationDrepCredentialRelation" +JOIN "StakeCredential" ON stake_credential = "StakeCredential".id +JOIN "Transaction" ON "Transaction".id = "StakeDelegationDrepCredentialRelation".tx_id +JOIN "Block" ON "Transaction".block_id = "Block".id +WHERE + "StakeCredential".credential = :credential! AND + "Block".slot <= :slot! +ORDER BY "Transaction".id DESC +LIMIT 1; diff --git a/webserver/server/app/models/governance/votesForAddress.queries.ts b/webserver/server/app/models/governance/votesForAddress.queries.ts new file mode 100644 index 00000000..5553a171 --- /dev/null +++ b/webserver/server/app/models/governance/votesForAddress.queries.ts @@ -0,0 +1,97 @@ +/** Types generated for queries found in "app/models/governance/votesForAddress.sql" */ +import { PreparedQuery } from '@pgtyped/runtime'; + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +export type NumberOrString = number | string; + +/** 'VotesForAddress' parameters type */ +export interface IVotesForAddressParams { + before_tx_id?: NumberOrString | null | void; + limit: NumberOrString; + until_tx_id: NumberOrString; + voter: Buffer; +} + +/** 'VotesForAddress' return type */ +export interface IVotesForAddressResult { + block: string; + txId: string; + votes: Json; +} + +/** 'VotesForAddress' query type */ +export interface IVotesForAddressQuery { + params: IVotesForAddressParams; + result: IVotesForAddressResult; +} + +const votesForAddressIR: any = {"usedParamSet":{"before_tx_id":true,"until_tx_id":true,"voter":true,"limit":true},"params":[{"name":"before_tx_id","required":false,"transform":{"type":"scalar"},"locs":[{"a":399,"b":411}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":427,"b":439}]},{"name":"voter","required":true,"transform":{"type":"scalar"},"locs":[{"a":457,"b":463}]},{"name":"limit","required":true,"transform":{"type":"scalar"},"locs":[{"a":506,"b":512}]}],"statement":"SELECT \n json_agg(\n json_build_object(\n 'govActionId', encode(gov_action_id, 'hex'),\n 'vote', encode(vote, 'hex')\n )\n ) as \"votes!\", \n encode(tx.hash, 'hex') as \"txId!\",\n MIN(encode(\"Block\".hash, 'hex')) as \"block!\"\nFROM \"GovernanceVote\"\nJOIN \"Transaction\" tx ON tx.id = \"GovernanceVote\".tx_id\nJOIN \"Block\" ON \"Block\".id = tx.block_id\nWHERE\n\ttx.id < :before_tx_id AND\n\ttx.id <= :until_tx_id! AND\n voter = :voter!\nGROUP BY tx.id\nORDER BY tx.id DESC\nLIMIT :limit!"}; + +/** + * Query generated from SQL: + * ``` + * SELECT + * json_agg( + * json_build_object( + * 'govActionId', encode(gov_action_id, 'hex'), + * 'vote', encode(vote, 'hex') + * ) + * ) as "votes!", + * encode(tx.hash, 'hex') as "txId!", + * MIN(encode("Block".hash, 'hex')) as "block!" + * FROM "GovernanceVote" + * JOIN "Transaction" tx ON tx.id = "GovernanceVote".tx_id + * JOIN "Block" ON "Block".id = tx.block_id + * WHERE + * tx.id < :before_tx_id AND + * tx.id <= :until_tx_id! AND + * voter = :voter! + * GROUP BY tx.id + * ORDER BY tx.id DESC + * LIMIT :limit! + * ``` + */ +export const votesForAddress = new PreparedQuery(votesForAddressIR); + + +/** 'DidVote' parameters type */ +export interface IDidVoteParams { + gov_action_ids: readonly (Buffer)[]; + until_tx_id: NumberOrString; + voter: Buffer; +} + +/** 'DidVote' return type */ +export interface IDidVoteResult { + govActionId: Buffer; + txId: string; + vote: Buffer; +} + +/** 'DidVote' query type */ +export interface IDidVoteQuery { + params: IDidVoteParams; + result: IDidVoteResult; +} + +const didVoteIR: any = {"usedParamSet":{"until_tx_id":true,"voter":true,"gov_action_ids":true},"params":[{"name":"gov_action_ids","required":true,"transform":{"type":"array_spread"},"locs":[{"a":293,"b":308}]},{"name":"until_tx_id","required":true,"transform":{"type":"scalar"},"locs":[{"a":230,"b":242}]},{"name":"voter","required":true,"transform":{"type":"scalar"},"locs":[{"a":260,"b":266}]}],"statement":"SELECT gov_action_id as \"govActionId!\",\n vote as \"vote!\",\n encode(\"Transaction\".hash, 'hex') as \"txId!\"\nFROM \"GovernanceVote\"\nJOIN \"Transaction\" ON \"GovernanceVote\".tx_id = \"Transaction\".id\nWHERE\n\t\"Transaction\".id <= :until_tx_id! AND\n voter = :voter! AND\n gov_action_id IN :gov_action_ids!\nORDER BY \"Transaction\".id"}; + +/** + * Query generated from SQL: + * ``` + * SELECT gov_action_id as "govActionId!", + * vote as "vote!", + * encode("Transaction".hash, 'hex') as "txId!" + * FROM "GovernanceVote" + * JOIN "Transaction" ON "GovernanceVote".tx_id = "Transaction".id + * WHERE + * "Transaction".id <= :until_tx_id! AND + * voter = :voter! AND + * gov_action_id IN :gov_action_ids! + * ORDER BY "Transaction".id + * ``` + */ +export const didVote = new PreparedQuery(didVoteIR); + + diff --git a/webserver/server/app/models/governance/votesForAddress.sql b/webserver/server/app/models/governance/votesForAddress.sql new file mode 100644 index 00000000..407d93f5 --- /dev/null +++ b/webserver/server/app/models/governance/votesForAddress.sql @@ -0,0 +1,36 @@ +/* @name votesForAddress */ +SELECT + json_agg( + json_build_object( + 'govActionId', encode(gov_action_id, 'hex'), + 'vote', encode(vote, 'hex') + ) + ) as "votes!", + encode(tx.hash, 'hex') as "txId!", + MIN(encode("Block".hash, 'hex')) as "block!" +FROM "GovernanceVote" +JOIN "Transaction" tx ON tx.id = "GovernanceVote".tx_id +JOIN "Block" ON "Block".id = tx.block_id +WHERE + tx.id < :before_tx_id AND + tx.id <= :until_tx_id! AND + voter = :voter! +GROUP BY tx.id +ORDER BY tx.id DESC +LIMIT :limit!; + + +/* +@name didVote +@param gov_action_ids -> (...) +*/ +SELECT gov_action_id as "govActionId!", + vote as "vote!", + encode("Transaction".hash, 'hex') as "txId!" +FROM "GovernanceVote" +JOIN "Transaction" ON "GovernanceVote".tx_id = "Transaction".id +WHERE + "Transaction".id <= :until_tx_id! AND + voter = :voter! AND + gov_action_id IN :gov_action_ids! +ORDER BY "Transaction".id; \ No newline at end of file diff --git a/webserver/server/app/services/DrepDelegationForAddress.ts b/webserver/server/app/services/DrepDelegationForAddress.ts new file mode 100644 index 00000000..119c5193 --- /dev/null +++ b/webserver/server/app/services/DrepDelegationForAddress.ts @@ -0,0 +1,10 @@ +import type { PoolClient } from 'pg'; +import { ISqlDrepStakeDelegationForAddressResult, sqlDrepStakeDelegationForAddress } from '../models/delegation/drepDelegationForAddress.queries'; + +export async function drepDelegationForAddress(request: { + address: Buffer, + until: { absoluteSlot: number }, + dbTx: PoolClient, +}): Promise { + return (await sqlDrepStakeDelegationForAddress.run({ credential: request.address, slot: request.until.absoluteSlot }, request.dbTx))[0]; +} \ No newline at end of file diff --git a/webserver/server/app/services/GovernanceCredentialVotesByActionIds.ts b/webserver/server/app/services/GovernanceCredentialVotesByActionIds.ts new file mode 100644 index 00000000..a83be3a3 --- /dev/null +++ b/webserver/server/app/services/GovernanceCredentialVotesByActionIds.ts @@ -0,0 +1,24 @@ +import type { PoolClient } from 'pg'; +import { + didVote, + IDidVoteResult, +} from '../models/governance/votesForAddress.queries'; + +export async function governanceCredentialDidVote(request: { + credential: Buffer; + govActionIds: Buffer[]; + dbTx: PoolClient; + until: number; +}): Promise { + return ( + await didVote.run( + { + voter: request.credential, + gov_action_ids: request.govActionIds, + until_tx_id: request.until, + }, + request.dbTx + ) + ); +} + diff --git a/webserver/server/app/services/GovernanceVotesForAddress.ts b/webserver/server/app/services/GovernanceVotesForAddress.ts new file mode 100644 index 00000000..879b106b --- /dev/null +++ b/webserver/server/app/services/GovernanceVotesForAddress.ts @@ -0,0 +1,25 @@ +import type { PoolClient } from 'pg'; +import { + IVotesForAddressResult, + votesForAddress, +} from '../models/governance/votesForAddress.queries'; + +export async function governanceVotesForAddress(request: { + credential: Buffer; + dbTx: PoolClient; + limit: number; + before: number; + until: number; +}): Promise { + return ( + await votesForAddress.run( + { + voter: request.credential, + limit: request.limit, + before_tx_id: request.before, + until_tx_id: request.until, + }, + request.dbTx + ) + ); +} diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index da409050..806f8799 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -43,4 +43,12 @@ export const ASSET_UTXOS_LIMIT = { export const MINT_BURN_HISTORY_LIMIT = { DEFAULT_PAGE_SIZE: 50, +}; + +export const GOVERNANCE_VOTES_LIMIT = { + DEFAULT_PAGE_SIZE: 50, +}; + +export const GOVERNANCE_VOTES_BY_GOV_IDS_LIMIT = { + MAX_ACTION_IDS: 50, }; \ No newline at end of file diff --git a/webserver/shared/errors.ts b/webserver/shared/errors.ts index 53b556bf..87742c4c 100644 --- a/webserver/shared/errors.ts +++ b/webserver/shared/errors.ts @@ -107,6 +107,12 @@ export const Errors = { detailsGen: (details: { limit: number; found: number }) => `Limit of ${details.limit}, found ${details.found}`, }, + GovActionIdsLimitExceeded: { + code: ErrorCodes.AssetLimitExceeded, + prefix: "Exceeded request governance action ids limit.", + detailsGen: (details: { limit: number; found: number }) => + `Limit of ${details.limit}, found ${details.found}`, + }, } as const; export function genErrorMessage( diff --git a/webserver/shared/models/DelegationForAddress.ts b/webserver/shared/models/DelegationForAddress.ts index 01c5c914..1959f50a 100644 --- a/webserver/shared/models/DelegationForAddress.ts +++ b/webserver/shared/models/DelegationForAddress.ts @@ -8,4 +8,9 @@ export type DelegationForAddressRequest = { export type DelegationForAddressResponse = { pool: string | null; txId: string | null; +}; + +export type DrepDelegationForAddressResponse = { + drep: string | null; + txId: string | null; }; \ No newline at end of file diff --git a/webserver/shared/models/Governance.ts b/webserver/shared/models/Governance.ts new file mode 100644 index 00000000..72c8e86a --- /dev/null +++ b/webserver/shared/models/Governance.ts @@ -0,0 +1,25 @@ +import { CredentialHex } from "./Address"; +import { AfterBlockPagination, UntilBlockPagination } from "./common"; + +export type GovernanceVotesForCredentialRequest = { + credential: CredentialHex; + limit?: number | undefined; +} & UntilBlockPagination & + AfterBlockPagination; + +export type GovernanceVotesForCredentialResponse = { + votes: { govActionId: string; vote: string }[]; + txId: string; + block: string; +}[]; + +export type GovernanceCredentialDidVoteRequest = { + credential: CredentialHex; + actionIds: string[]; +} & UntilBlockPagination; + +export type GovernanceCredentialDidVoteResponse = { + actionId: string; + txId: string; + payload: string; +}[]; diff --git a/webserver/shared/models/common.ts b/webserver/shared/models/common.ts index e5fefd61..5a5c7d22 100644 --- a/webserver/shared/models/common.ts +++ b/webserver/shared/models/common.ts @@ -36,6 +36,9 @@ export enum RelationFilterType { UnusedOutputStake = 0b10000000000000000000, ReferenceInput = 0b100000000000000000000, ReferenceInputStake = 0b1000000000000000000000, + DrepStakeDelegation = 0b10000000000000000000000, + DrepStakeDelegationTarget = 0b100000000000000000000000, + DrepOperation = 0b1000000000000000000000000, NO_FILTER = 0xff, } diff --git a/webserver/shared/routes.ts b/webserver/shared/routes.ts index 9088b507..dd43d241 100644 --- a/webserver/shared/routes.ts +++ b/webserver/shared/routes.ts @@ -7,9 +7,15 @@ import type { CredentialAddressRequest, CredentialAddressResponse, } from "./models/CredentialAddress"; -import { DexMeanPriceRequest, DexMeanPriceResponse } from "./models/DexMeanPrice"; +import { + DexMeanPriceRequest, + DexMeanPriceResponse, +} from "./models/DexMeanPrice"; import { DexSwapRequest, DexSwapResponse } from "./models/DexSwap"; -import { DexLastPriceRequest, DexLastPriceResponse } from "./models/DexLastPrice"; +import { + DexLastPriceRequest, + DexLastPriceResponse, +} from "./models/DexLastPrice"; import { Cip25Response, PolicyIdAssetMapType } from "./models/PolicyIdAssetMap"; import type { TransactionHistoryRequest, @@ -22,6 +28,7 @@ import type { import type { DelegationForAddressRequest, DelegationForAddressResponse, + DrepDelegationForAddressResponse, } from "./models/DelegationForAddress"; import type { DelegationForPoolRequest, @@ -36,6 +43,12 @@ import type { MintBurnHistoryRequest, MintBurnHistoryResponse, } from "./models/MintBurn"; +import { + GovernanceCredentialDidVoteRequest, + GovernanceCredentialDidVoteResponse, + GovernanceVotesForCredentialRequest, + GovernanceVotesForCredentialResponse, +} from "./models/Governance"; export enum Routes { transactionHistory = "transaction/history", @@ -52,6 +65,9 @@ export enum Routes { projectedNftEventsRange = "projected-nft/range", assetUtxos = "asset/utxos", mintBurnHistory = "asset/mint-burn-history", + drepDelegationForAddress = "delegation/drep/address", + governanceVotesForCredential = "governance/credential/votes", + governanceCredentialVotesByGovActionId = "governance/credential/votesByGovId", } export type EndpointTypes = { @@ -125,4 +141,19 @@ export type EndpointTypes = { input: MintBurnHistoryRequest; response: MintBurnHistoryResponse; }; + [Routes.drepDelegationForAddress]: { + name: typeof Routes.drepDelegationForAddress; + input: DelegationForAddressRequest; + response: DrepDelegationForAddressResponse; + }; + [Routes.governanceVotesForCredential]: { + name: typeof Routes.governanceVotesForCredential; + input: GovernanceVotesForCredentialRequest; + response: GovernanceVotesForCredentialResponse; + }; + [Routes.governanceCredentialVotesByGovActionId]: { + name: typeof Routes.governanceCredentialVotesByGovActionId; + input: GovernanceCredentialDidVoteRequest; + response: GovernanceCredentialDidVoteResponse; + }; };