From e27623109634cd1444dc8d54631220f2597c402c Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 3 Oct 2023 15:45:30 -0300 Subject: [PATCH 01/12] introduce new relation that tracks stake delegation certs --- indexer/entity/src/lib.rs | 1 + indexer/entity/src/stake_delegation.rs | 37 ++++++ indexer/execution_plans/default.toml | 2 + indexer/migration/src/lib.rs | 2 + ...27_231206_create_stake_delegation_table.rs | 74 ++++++++++++ indexer/tasks/src/multiera/mod.rs | 1 + .../multiera/multiera_address_delegation.rs | 113 ++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 indexer/entity/src/stake_delegation.rs create mode 100644 indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs create mode 100644 indexer/tasks/src/multiera/multiera_address_delegation.rs diff --git a/indexer/entity/src/lib.rs b/indexer/entity/src/lib.rs index 3cb250a0..423266f4 100644 --- a/indexer/entity/src/lib.rs +++ b/indexer/entity/src/lib.rs @@ -15,4 +15,5 @@ pub mod dex_swap; pub mod native_asset; pub mod plutus_data; pub mod plutus_data_hash; +pub mod stake_delegation; pub mod transaction_metadata; diff --git a/indexer/entity/src/stake_delegation.rs b/indexer/entity/src/stake_delegation.rs new file mode 100644 index 00000000..3f069a5f --- /dev/null +++ b/indexer/entity/src/stake_delegation.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 = "StakeDelegationCredentialRelation")] +pub struct Model { + #[sea_orm(primary_key, column_type = "BigInteger")] + pub id: i64, + pub stake_credential: i64, + // pool registrations are not tracked in StakeCredentials, + pub pool_credential: Option>, + pub tx_id: i64, +} + +#[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/execution_plans/default.toml b/indexer/execution_plans/default.toml index 70e98cbd..c9cbdbf0 100644 --- a/indexer/execution_plans/default.toml +++ b/indexer/execution_plans/default.toml @@ -63,3 +63,5 @@ readonly=false readonly=false [MultieraCip25EntryTask] + +[MultieraAddressDelegationTask] diff --git a/indexer/migration/src/lib.rs b/indexer/migration/src/lib.rs index 884a1c7d..15d1f8f7 100644 --- a/indexer/migration/src/lib.rs +++ b/indexer/migration/src/lib.rs @@ -17,6 +17,7 @@ mod m20220528_000012_create_plutus_data_table; mod m20220808_000013_create_transaction_reference_input_table; mod m20221031_000014_create_dex_table; mod m20230223_000015_modify_block_table; +mod m20230927_231206_create_stake_delegation_table; pub struct Migrator; @@ -41,6 +42,7 @@ impl MigratorTrait for Migrator { Box::new(m20220808_000013_create_transaction_reference_input_table::Migration), Box::new(m20221031_000014_create_dex_table::Migration), Box::new(m20230223_000015_modify_block_table::Migration), + Box::new(m20230927_231206_create_stake_delegation_table::Migration), ] } } diff --git a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs new file mode 100644 index 00000000..59301301 --- /dev/null +++ b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs @@ -0,0 +1,74 @@ +use entity::prelude::{StakeCredential, StakeCredentialColumn, Transaction, TransactionColumn}; +use entity::stake_delegation::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20230927_231206_create_stake_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::PoolCredential).binary()) + .foreign_key( + ForeignKey::create() + .name("fk-stake_delegation-credential_id") + .from(Entity, Column::StakeCredential) + .to(StakeCredential, StakeCredentialColumn::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-stake_delegation-tx_id") + .from(Entity, Column::TxId) + .to(Transaction, TransactionColumn::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .primary_key( + Index::create() + .table(Entity) + .name("stake_delegation_credential-pk") + .col(Column::Id), + ) + .to_owned(), + ) + .await + + // manager + // .create_index( + // Index::create() + // .table(Entity) + // .name("index-address_credential-credential") + // .col(Column::CredentialId) + // .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/tasks/src/multiera/mod.rs b/indexer/tasks/src/multiera/mod.rs index 0ccf716b..f6fc1a41 100644 --- a/indexer/tasks/src/multiera/mod.rs +++ b/indexer/tasks/src/multiera/mod.rs @@ -1,6 +1,7 @@ pub mod dex; pub mod multiera_address; pub mod multiera_address_credential_relations; +pub mod multiera_address_delegation; pub mod multiera_asset_mint; pub mod multiera_block; pub mod multiera_cip25entry; diff --git a/indexer/tasks/src/multiera/multiera_address_delegation.rs b/indexer/tasks/src/multiera/multiera_address_delegation.rs new file mode 100644 index 00000000..5744813c --- /dev/null +++ b/indexer/tasks/src/multiera/multiera_address_delegation.rs @@ -0,0 +1,113 @@ +use crate::{ + multiera::multiera_stake_credentials::MultieraStakeCredentialTask, + types::{AddressCredentialRelationValue, TxCredentialRelationValue}, +}; +use cardano_multiplatform_lib::{ + address::{BaseAddress, EnterpriseAddress, PointerAddress, RewardAddress}, + byron::ByronAddress, +}; +use entity::{ + prelude::*, + sea_orm::{prelude::*, DatabaseTransaction}, +}; +use pallas::ledger::{ + primitives::{alonzo::Certificate, Fragment}, + traverse::{MultiEraBlock, MultiEraCert, MultiEraOutput, MultiEraTx}, +}; +use sea_orm::{Order, QueryOrder, Set}; +use std::collections::{BTreeMap, BTreeSet}; +use std::ops::Deref; + +use super::{ + 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 MultieraAddressDelegationTask; + configuration EmptyConfig; + doc "Tracks stake delegation actions to pools."; + era multiera; + dependencies [MultieraStakeCredentialTask]; + read [multiera_txs, multiera_stake_credential]; + write []; + should_add_task |block, _properties| { + // recall: txs may have no outputs if they just burn all inputs as fee + // TODO: this runs slightly more than it should + !block.1.is_empty() + }; + 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<'_, MultiEraBlock<'_>, BlockGlobalInfo>, + multiera_txs: &[TransactionModel], + multiera_stake_credential: &BTreeMap, StakeCredentialModel>, +) -> Result<(), DbErr> { + for (tx_body, cardano_transaction) in block.1.txs().iter().zip(multiera_txs) { + for cert in tx_body.certs() { + { + let tx_id = cardano_transaction.id; + let cert = &cert; + match cert.as_alonzo().unwrap() { + Certificate::StakeDelegation(credential, pool) => { + let credential = credential.encode_fragment().unwrap(); + + let stake_credential_id = multiera_stake_credential + .get(&credential.to_vec()) + .unwrap() + .id; + + entity::stake_delegation::ActiveModel { + stake_credential: Set(stake_credential_id), + pool_credential: Set(Some(pool.to_vec())), + tx_id: Set(tx_id), + ..Default::default() + } + .save(db_tx) + .await?; + } + Certificate::StakeRegistration(credential) => {} + Certificate::StakeDeregistration(credential) => { + let credential = credential.encode_fragment().unwrap(); + + let stake_credential_id = multiera_stake_credential + .get(&credential.to_vec()) + .unwrap() + .id; + + entity::stake_delegation::ActiveModel { + stake_credential: Set(stake_credential_id), + pool_credential: Set(None), + tx_id: Set(tx_id), + ..Default::default() + } + .save(db_tx) + .await?; + } + Certificate::PoolRegistration { + operator, + pool_owners, + reward_account, + .. + } => {} + Certificate::PoolRetirement(key_hash, _) => {} + Certificate::GenesisKeyDelegation(_, _, _) => {} + Certificate::MoveInstantaneousRewardsCert(mir) => {} + }; + }; + } + } + + Ok(()) +} From 9d1dda3d088bc7c22c194d3d3a240876116cebe5 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 3 Oct 2023 17:09:57 -0300 Subject: [PATCH 02/12] add 'delegation/address' route --- .../DelegationForAddressController.ts | 59 +++++++++++++++++++ .../delegationForAddress.queries.ts | 41 +++++++++++++ .../delegation/delegationForAddress.sql | 11 ++++ webserver/server/app/services/Delegation.ts | 12 ++++ .../shared/models/DelegationForAddress.ts | 11 ++++ webserver/shared/routes.ts | 12 +++- 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 webserver/server/app/controllers/DelegationForAddressController.ts create mode 100644 webserver/server/app/models/delegation/delegationForAddress.queries.ts create mode 100644 webserver/server/app/models/delegation/delegationForAddress.sql create mode 100644 webserver/server/app/services/Delegation.ts create mode 100644 webserver/shared/models/DelegationForAddress.ts diff --git a/webserver/server/app/controllers/DelegationForAddressController.ts b/webserver/server/app/controllers/DelegationForAddressController.ts new file mode 100644 index 00000000..0fd43a63 --- /dev/null +++ b/webserver/server/app/controllers/DelegationForAddressController.ts @@ -0,0 +1,59 @@ +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 { getAddressTypes } from '../models/utils'; +import { delegationForAddress } from '../services/Delegation'; +import { DelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; + +const route = Routes.delegationForAddress; + +@Route('delegation/address') +export class DelegationForAddressController extends Controller { + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async delegationForAddress( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + const addressTypes = getAddressTypes([requestBody.address]); + + if (addressTypes.invalid.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.UNPROCESSABLE_ENTITY, + genErrorMessage(Errors.IncorrectAddressFormat, { + addresses: addressTypes.invalid, + }) + ); + } + + const response = await tx< + DelegationForAddressResponse + >(pool, async dbTx => { + const data = await delegationForAddress({ + address: addressTypes.credentialHex.map(addr => Buffer.from(addr, 'hex'))[0], + until: requestBody.until, + dbTx + }); + + return { + pool: data ? data.pool : null, + txId: data ? data.tx_id : null, + } + }); + + return response; + } +} + diff --git a/webserver/server/app/models/delegation/delegationForAddress.queries.ts b/webserver/server/app/models/delegation/delegationForAddress.queries.ts new file mode 100644 index 00000000..029cd3cd --- /dev/null +++ b/webserver/server/app/models/delegation/delegationForAddress.queries.ts @@ -0,0 +1,41 @@ +/** Types generated for queries found in "app/models/delegation/delegationForAddress.sql" */ +import { PreparedQuery } from '@pgtyped/query'; + +/** 'SqlStakeDelegation' parameters type */ +export interface ISqlStakeDelegationParams { + credential: Buffer; + slot: number; +} + +/** 'SqlStakeDelegation' return type */ +export interface ISqlStakeDelegationResult { + pool: string | null; + tx_id: string | null; +} + +/** 'SqlStakeDelegation' query type */ +export interface ISqlStakeDelegationQuery { + params: ISqlStakeDelegationParams; + result: ISqlStakeDelegationResult; +} + +const sqlStakeDelegationIR: any = {"usedParamSet":{"credential":true,"slot":true},"params":[{"name":"credential","required":true,"transform":{"type":"scalar"},"locs":[{"a":371,"b":382}]},{"name":"slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":405,"b":410}]}],"statement":"SELECT encode(pool_credential, 'hex') as pool, encode(\"Transaction\".hash, 'hex') as tx_id\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n\t\"StakeCredential\".credential = :credential! AND\n\t\"Block\".slot <= :slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) DESC\nLIMIT 1"}; + +/** + * Query generated from SQL: + * ``` + * SELECT encode(pool_credential, 'hex') as pool, encode("Transaction".hash, 'hex') as tx_id + * FROM "StakeDelegationCredentialRelation" + * JOIN "StakeCredential" ON stake_credential = "StakeCredential".id + * JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * "StakeCredential".credential = :credential! AND + * "Block".slot <= :slot! + * ORDER BY ("Block".height, "Transaction".tx_index) DESC + * LIMIT 1 + * ``` + */ +export const sqlStakeDelegation = new PreparedQuery(sqlStakeDelegationIR); + + diff --git a/webserver/server/app/models/delegation/delegationForAddress.sql b/webserver/server/app/models/delegation/delegationForAddress.sql new file mode 100644 index 00000000..fb4d993d --- /dev/null +++ b/webserver/server/app/models/delegation/delegationForAddress.sql @@ -0,0 +1,11 @@ +/* @name sqlStakeDelegation */ +SELECT encode(pool_credential, 'hex') as pool, encode("Transaction".hash, 'hex') as tx_id +FROM "StakeDelegationCredentialRelation" +JOIN "StakeCredential" ON stake_credential = "StakeCredential".id +JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id +JOIN "Block" ON "Transaction".block_id = "Block".id +WHERE + "StakeCredential".credential = :credential! AND + "Block".slot <= :slot! +ORDER BY ("Block".height, "Transaction".tx_index) DESC +LIMIT 1; \ No newline at end of file diff --git a/webserver/server/app/services/Delegation.ts b/webserver/server/app/services/Delegation.ts new file mode 100644 index 00000000..7b05fb8b --- /dev/null +++ b/webserver/server/app/services/Delegation.ts @@ -0,0 +1,12 @@ + +import type { PoolClient } from 'pg'; +import { ISqlStakeDelegationResult, sqlStakeDelegation } from '../models/delegation/delegationForAddress.queries'; + + +export async function delegationForAddress(request: { + address: Buffer, + until: { absoluteSlot: number }, + dbTx: PoolClient, +}): Promise { + return (await sqlStakeDelegation.run({ credential: request.address, slot: request.until.absoluteSlot }, request.dbTx))[0]; +} \ No newline at end of file diff --git a/webserver/shared/models/DelegationForAddress.ts b/webserver/shared/models/DelegationForAddress.ts new file mode 100644 index 00000000..01c5c914 --- /dev/null +++ b/webserver/shared/models/DelegationForAddress.ts @@ -0,0 +1,11 @@ +import { Address } from "./Address"; + +export type DelegationForAddressRequest = { + address: Address; + until: { absoluteSlot: number } +}; + +export type DelegationForAddressResponse = { + pool: string | null; + txId: string | null; +}; \ No newline at end of file diff --git a/webserver/shared/routes.ts b/webserver/shared/routes.ts index 1d7b099f..eca076ae 100644 --- a/webserver/shared/routes.ts +++ b/webserver/shared/routes.ts @@ -19,6 +19,10 @@ import type { TransactionOutputRequest, TransactionOutputResponse, } from "./models/TransactionOutput"; +import type { + DelegationForAddressRequest, + DelegationForAddressResponse, +} from "./models/DelegationForAddress"; export enum Routes { transactionHistory = "transaction/history", @@ -29,7 +33,8 @@ export enum Routes { metadataNft = "metadata/nft", dexMeanPrice = "dex/mean-price", dexSwap = "dex/swap", - dexLastPrice = "dex/last-price" + dexLastPrice = "dex/last-price", + delegationForAddress = "delegation/address", } export type EndpointTypes = { @@ -78,4 +83,9 @@ export type EndpointTypes = { input: DexLastPriceRequest; response: DexLastPriceResponse; }; + [Routes.delegationForAddress]: { + name: typeof Routes.delegationForAddress; + input: DelegationForAddressRequest; + response: DelegationForAddressResponse; + }; }; From d09923b9a2161994aace2b5e157220fe4c808d4b Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Thu, 5 Oct 2023 16:51:18 -0300 Subject: [PATCH 03/12] regen docs/bin/openapi.json --- docs/bin/openapi.json | 100 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index 89e31646..d85aed2f 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -282,6 +282,47 @@ } ] }, + "DelegationForAddressResponse": { + "properties": { + "txId": { + "type": "string", + "nullable": true + }, + "pool": { + "type": "string", + "nullable": true + } + }, + "required": [ + "txId", + "pool" + ], + "type": "object" + }, + "DelegationForAddressRequest": { + "properties": { + "until": { + "properties": { + "absoluteSlot": { + "type": "number", + "format": "double" + } + }, + "required": [ + "absoluteSlot" + ], + "type": "object" + }, + "address": { + "$ref": "#/components/schemas/Address" + } + }, + "required": [ + "until", + "address" + ], + "type": "object" + }, "PolicyId": { "type": "string", "example": "b863bc7369f46136ac1048adb2fa7dae3af944c3bbb2be2f216a8d4f", @@ -1008,6 +1049,65 @@ } } }, + "/delegation/address": { + "post": { + "operationId": "DelegationForAddress", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationForAddressResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + }, + "422": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + } + }, + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationForAddressRequest" + } + } + } + } + } + }, "/dex/last-price": { "post": { "operationId": "DexLastPrice", From df472edb9d97c23258c17cc4b3b1955f15ff3543 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Thu, 5 Oct 2023 16:51:43 -0300 Subject: [PATCH 04/12] remove copy-pasted comment in migration --- .../m20230927_231206_create_stake_delegation_table.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs index 59301301..070a5aa7 100644 --- a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs +++ b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs @@ -54,16 +54,6 @@ impl MigrationTrait for Migration { .to_owned(), ) .await - - // manager - // .create_index( - // Index::create() - // .table(Entity) - // .name("index-address_credential-credential") - // .col(Column::CredentialId) - // .to_owned(), - // ) - // .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { From 50b497f9133271ec7c55b90bec946033c111a6f1 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Thu, 5 Oct 2023 21:06:27 -0300 Subject: [PATCH 05/12] cleanup unused branches/variants of certificates --- .../tasks/src/multiera/multiera_address_delegation.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/indexer/tasks/src/multiera/multiera_address_delegation.rs b/indexer/tasks/src/multiera/multiera_address_delegation.rs index 5744813c..cf000fda 100644 --- a/indexer/tasks/src/multiera/multiera_address_delegation.rs +++ b/indexer/tasks/src/multiera/multiera_address_delegation.rs @@ -77,7 +77,6 @@ async fn handle( .save(db_tx) .await?; } - Certificate::StakeRegistration(credential) => {} Certificate::StakeDeregistration(credential) => { let credential = credential.encode_fragment().unwrap(); @@ -95,15 +94,7 @@ async fn handle( .save(db_tx) .await?; } - Certificate::PoolRegistration { - operator, - pool_owners, - reward_account, - .. - } => {} - Certificate::PoolRetirement(key_hash, _) => {} - Certificate::GenesisKeyDelegation(_, _, _) => {} - Certificate::MoveInstantaneousRewardsCert(mir) => {} + _ => {} }; }; } From d7a107800223b429677a92c0348f37f5286ce42c Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Wed, 11 Oct 2023 21:54:01 -0300 Subject: [PATCH 06/12] add previous pool column this helps getting un-delegation events by pool, since otherwise re-delegation doesn't directly address the pool that no longer has the delegation. --- indexer/entity/src/stake_delegation.rs | 1 + ...27_231206_create_stake_delegation_table.rs | 1 + .../multiera/multiera_address_delegation.rs | 70 ++++++++++--------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/indexer/entity/src/stake_delegation.rs b/indexer/entity/src/stake_delegation.rs index 3f069a5f..d5257e0f 100644 --- a/indexer/entity/src/stake_delegation.rs +++ b/indexer/entity/src/stake_delegation.rs @@ -10,6 +10,7 @@ pub struct Model { // pool registrations are not tracked in StakeCredentials, pub pool_credential: Option>, pub tx_id: i64, + pub previous_pool: Option>, } #[derive(Copy, Clone, Debug, DeriveRelation, EnumIter)] diff --git a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs index 070a5aa7..1f490f8a 100644 --- a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs +++ b/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs @@ -31,6 +31,7 @@ impl MigrationTrait for Migration { ) .col(ColumnDef::new(Column::TxId).big_integer().not_null()) .col(ColumnDef::new(Column::PoolCredential).binary()) + .col(ColumnDef::new(Column::PreviousPool).binary()) .foreign_key( ForeignKey::create() .name("fk-stake_delegation-credential_id") diff --git a/indexer/tasks/src/multiera/multiera_address_delegation.rs b/indexer/tasks/src/multiera/multiera_address_delegation.rs index cf000fda..a74faaae 100644 --- a/indexer/tasks/src/multiera/multiera_address_delegation.rs +++ b/indexer/tasks/src/multiera/multiera_address_delegation.rs @@ -59,43 +59,49 @@ async fn handle( { let tx_id = cardano_transaction.id; let cert = &cert; - match cert.as_alonzo().unwrap() { - Certificate::StakeDelegation(credential, pool) => { - let credential = credential.encode_fragment().unwrap(); + let (credential, pool) = match cert.as_alonzo().unwrap() { + Certificate::StakeDelegation(credential, pool) => (credential, Some(pool)), + Certificate::StakeDeregistration(credential) => (credential, None), + _ => continue, + }; - let stake_credential_id = multiera_stake_credential - .get(&credential.to_vec()) - .unwrap() - .id; + let credential = credential.encode_fragment().unwrap(); - entity::stake_delegation::ActiveModel { - stake_credential: Set(stake_credential_id), - pool_credential: Set(Some(pool.to_vec())), - tx_id: Set(tx_id), - ..Default::default() - } - .save(db_tx) - .await?; - } - Certificate::StakeDeregistration(credential) => { - let credential = credential.encode_fragment().unwrap(); + let stake_credential_id = multiera_stake_credential + .get(&credential.to_vec()) + .unwrap() + .id; + + let previous_entry = entity::stake_delegation::Entity::find() + .filter( + entity::stake_delegation::Column::StakeCredential.eq(stake_credential_id), + ) + .order_by_desc(entity::stake_delegation::Column::Id) + .one(db_tx) + .await?; - let stake_credential_id = multiera_stake_credential - .get(&credential.to_vec()) - .unwrap() - .id; + let pool = pool.map(|pool| pool.to_vec()); - entity::stake_delegation::ActiveModel { - stake_credential: Set(stake_credential_id), - pool_credential: Set(None), - tx_id: Set(tx_id), - ..Default::default() - } - .save(db_tx) - .await?; + if let Some((previous, pool)) = previous_entry + .as_ref() + .and_then(|entry| entry.pool_credential.as_ref()) + .zip(pool.as_ref()) + { + // re-delegating shouldn't have any effect. + if previous == pool { + continue; } - _ => {} - }; + } + + entity::stake_delegation::ActiveModel { + stake_credential: Set(stake_credential_id), + pool_credential: Set(pool.map(|pool| pool.to_vec())), + tx_id: Set(tx_id), + previous_pool: Set(previous_entry.and_then(|entity| entity.pool_credential)), + ..Default::default() + } + .save(db_tx) + .await?; }; } } From b9ad7ab32a8e696977826854bfad74df366f1cf5 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Wed, 11 Oct 2023 23:16:29 -0300 Subject: [PATCH 07/12] add endpoint get pool's delegation history by pool --- docs/bin/openapi.json | 122 ++++++++++++++++++ .../DelegationForAddressController.ts | 2 +- .../DelegationForPoolController.ts | 47 +++++++ .../delegationForAddress.queries.ts | 20 +-- .../delegation/delegationForAddress.sql | 2 +- .../delegation/delegationsForPool.queries.ts | 49 +++++++ .../models/delegation/delegationsForPool.sql | 20 +++ webserver/server/app/services/Delegation.ts | 12 -- .../app/services/DelegationForAddress.ts | 10 ++ .../server/app/services/DelegationForPool.ts | 15 +++ webserver/shared/models/DelegationForPool.ts | 13 ++ webserver/shared/models/Pool.ts | 8 ++ webserver/shared/routes.ts | 10 ++ 13 files changed, 306 insertions(+), 24 deletions(-) create mode 100644 webserver/server/app/controllers/DelegationForPoolController.ts create mode 100644 webserver/server/app/models/delegation/delegationsForPool.queries.ts create mode 100644 webserver/server/app/models/delegation/delegationsForPool.sql delete mode 100644 webserver/server/app/services/Delegation.ts create mode 100644 webserver/server/app/services/DelegationForAddress.ts create mode 100644 webserver/server/app/services/DelegationForPool.ts create mode 100644 webserver/shared/models/DelegationForPool.ts create mode 100644 webserver/shared/models/Pool.ts diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index d85aed2f..748efc5b 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -323,6 +323,69 @@ ], "type": "object" }, + "DelegationForPoolResponse": { + "items": { + "properties": { + "txId": { + "type": "string", + "nullable": true + }, + "isDelegation": { + "type": "boolean" + }, + "credential": { + "$ref": "#/components/schemas/Address" + } + }, + "required": [ + "txId", + "isDelegation", + "credential" + ], + "type": "object" + }, + "type": "array" + }, + "PoolHex": { + "type": "string", + "example": "8200581c8baf48931c5187cd59fde553f4e7da2e1a2aa9202ec6e67815cb3f8a", + "pattern": "[0-9a-fA-F]{56}" + }, + "Pool": { + "$ref": "#/components/schemas/PoolHex" + }, + "DelegationForPoolRequest": { + "properties": { + "range": { + "properties": { + "maxSlot": { + "type": "number", + "format": "double" + }, + "minSlot": { + "type": "number", + "format": "double" + } + }, + "required": [ + "maxSlot", + "minSlot" + ], + "type": "object" + }, + "pools": { + "items": { + "$ref": "#/components/schemas/Pool" + }, + "type": "array" + } + }, + "required": [ + "range", + "pools" + ], + "type": "object" + }, "PolicyId": { "type": "string", "example": "b863bc7369f46136ac1048adb2fa7dae3af944c3bbb2be2f216a8d4f", @@ -1108,6 +1171,65 @@ } } }, + "/delegation/pool": { + "post": { + "operationId": "DelegationForPool", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationForPoolResponse" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + }, + "409": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + }, + "422": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorShape" + } + } + } + } + }, + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationForPoolRequest" + } + } + } + } + } + }, "/dex/last-price": { "post": { "operationId": "DexLastPrice", diff --git a/webserver/server/app/controllers/DelegationForAddressController.ts b/webserver/server/app/controllers/DelegationForAddressController.ts index 0fd43a63..9569b9dd 100644 --- a/webserver/server/app/controllers/DelegationForAddressController.ts +++ b/webserver/server/app/controllers/DelegationForAddressController.ts @@ -8,7 +8,7 @@ import { Errors } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { getAddressTypes } from '../models/utils'; -import { delegationForAddress } from '../services/Delegation'; +import { delegationForAddress } from '../services/DelegationForAddress'; import { DelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; const route = Routes.delegationForAddress; diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts new file mode 100644 index 00000000..0a569e7d --- /dev/null +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -0,0 +1,47 @@ +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 { getAddressTypes } from '../models/utils'; +import { delegationsForPool } from '../services/DelegationForPool'; +import { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; + +const route = Routes.delegationForPool; + +@Route('delegation/pool') +export class DelegationForPoolController extends Controller { + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async delegationForPool( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + const response = await tx< + DelegationForPoolResponse + >(pool, async dbTx => { + const data = await delegationsForPool({ + pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')), + range: requestBody.range, + dbTx + }); + + return data.map(data => ({ + credential: data.credential as string, + isDelegation: data.is_delegation as boolean, + txId: data.tx_id as string, + })); + }); + + return response; + } +} diff --git a/webserver/server/app/models/delegation/delegationForAddress.queries.ts b/webserver/server/app/models/delegation/delegationForAddress.queries.ts index 029cd3cd..be551288 100644 --- a/webserver/server/app/models/delegation/delegationForAddress.queries.ts +++ b/webserver/server/app/models/delegation/delegationForAddress.queries.ts @@ -1,25 +1,25 @@ /** Types generated for queries found in "app/models/delegation/delegationForAddress.sql" */ import { PreparedQuery } from '@pgtyped/query'; -/** 'SqlStakeDelegation' parameters type */ -export interface ISqlStakeDelegationParams { +/** 'SqlStakeDelegationForAddress' parameters type */ +export interface ISqlStakeDelegationForAddressParams { credential: Buffer; slot: number; } -/** 'SqlStakeDelegation' return type */ -export interface ISqlStakeDelegationResult { +/** 'SqlStakeDelegationForAddress' return type */ +export interface ISqlStakeDelegationForAddressResult { pool: string | null; tx_id: string | null; } -/** 'SqlStakeDelegation' query type */ -export interface ISqlStakeDelegationQuery { - params: ISqlStakeDelegationParams; - result: ISqlStakeDelegationResult; +/** 'SqlStakeDelegationForAddress' query type */ +export interface ISqlStakeDelegationForAddressQuery { + params: ISqlStakeDelegationForAddressParams; + result: ISqlStakeDelegationForAddressResult; } -const sqlStakeDelegationIR: any = {"usedParamSet":{"credential":true,"slot":true},"params":[{"name":"credential","required":true,"transform":{"type":"scalar"},"locs":[{"a":371,"b":382}]},{"name":"slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":405,"b":410}]}],"statement":"SELECT encode(pool_credential, 'hex') as pool, encode(\"Transaction\".hash, 'hex') as tx_id\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n\t\"StakeCredential\".credential = :credential! AND\n\t\"Block\".slot <= :slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) DESC\nLIMIT 1"}; +const sqlStakeDelegationForAddressIR: any = {"usedParamSet":{"credential":true,"slot":true},"params":[{"name":"credential","required":true,"transform":{"type":"scalar"},"locs":[{"a":371,"b":382}]},{"name":"slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":405,"b":410}]}],"statement":"SELECT encode(pool_credential, 'hex') as pool, encode(\"Transaction\".hash, 'hex') as tx_id\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n\t\"StakeCredential\".credential = :credential! AND\n\t\"Block\".slot <= :slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) DESC\nLIMIT 1"}; /** * Query generated from SQL: @@ -36,6 +36,6 @@ const sqlStakeDelegationIR: any = {"usedParamSet":{"credential":true,"slot":true * LIMIT 1 * ``` */ -export const sqlStakeDelegation = new PreparedQuery(sqlStakeDelegationIR); +export const sqlStakeDelegationForAddress = new PreparedQuery(sqlStakeDelegationForAddressIR); diff --git a/webserver/server/app/models/delegation/delegationForAddress.sql b/webserver/server/app/models/delegation/delegationForAddress.sql index fb4d993d..3be67195 100644 --- a/webserver/server/app/models/delegation/delegationForAddress.sql +++ b/webserver/server/app/models/delegation/delegationForAddress.sql @@ -1,4 +1,4 @@ -/* @name sqlStakeDelegation */ +/* @name sqlStakeDelegationForAddress */ SELECT encode(pool_credential, 'hex') as pool, encode("Transaction".hash, 'hex') as tx_id FROM "StakeDelegationCredentialRelation" JOIN "StakeCredential" ON stake_credential = "StakeCredential".id diff --git a/webserver/server/app/models/delegation/delegationsForPool.queries.ts b/webserver/server/app/models/delegation/delegationsForPool.queries.ts new file mode 100644 index 00000000..75c9f0f9 --- /dev/null +++ b/webserver/server/app/models/delegation/delegationsForPool.queries.ts @@ -0,0 +1,49 @@ +/** Types generated for queries found in "app/models/delegation/delegationsForPool.sql" */ +import { PreparedQuery } from '@pgtyped/query'; + +/** 'SqlStakeDelegationByPool' parameters type */ +export interface ISqlStakeDelegationByPoolParams { + max_slot: number; + min_slot: number; + pools: readonly (Buffer)[]; +} + +/** 'SqlStakeDelegationByPool' return type */ +export interface ISqlStakeDelegationByPoolResult { + credential: string | null; + is_delegation: boolean | null; + tx_id: string | null; +} + +/** 'SqlStakeDelegationByPool' query type */ +export interface ISqlStakeDelegationByPoolQuery { + params: ISqlStakeDelegationByPoolParams; + result: ISqlStakeDelegationByPoolResult; +} + +const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot":true,"max_slot":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":160,"b":166},{"a":505,"b":511},{"a":572,"b":578}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":603,"b":612}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":635,"b":644}]}],"statement":"SELECT \n\tencode(credential, 'hex') as credential,\n\tencode(\"Transaction\".hash, 'hex') as tx_id,\n\tCOALESCE(\"StakeDelegationCredentialRelation\".pool_credential IN :pools!, false) as is_delegation\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n (\n\t\t\"StakeDelegationCredentialRelation\".pool_credential IN :pools! OR\n\t \t\"StakeDelegationCredentialRelation\".previous_pool IN :pools!\n\t) AND\n\t\"Block\".slot > :min_slot! AND\n\t\"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; + +/** + * Query generated from SQL: + * ``` + * SELECT + * encode(credential, 'hex') as credential, + * encode("Transaction".hash, 'hex') as tx_id, + * COALESCE("StakeDelegationCredentialRelation".pool_credential IN :pools!, false) as is_delegation + * FROM "StakeDelegationCredentialRelation" + * JOIN "StakeCredential" ON stake_credential = "StakeCredential".id + * JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id + * JOIN "Block" ON "Transaction".block_id = "Block".id + * WHERE + * ( + * "StakeDelegationCredentialRelation".pool_credential IN :pools! OR + * "StakeDelegationCredentialRelation".previous_pool IN :pools! + * ) AND + * "Block".slot > :min_slot! AND + * "Block".slot <= :max_slot! + * ORDER BY ("Block".height, "Transaction".tx_index) ASC + * ``` + */ +export const sqlStakeDelegationByPool = new PreparedQuery(sqlStakeDelegationByPoolIR); + + diff --git a/webserver/server/app/models/delegation/delegationsForPool.sql b/webserver/server/app/models/delegation/delegationsForPool.sql new file mode 100644 index 00000000..c2ea2a57 --- /dev/null +++ b/webserver/server/app/models/delegation/delegationsForPool.sql @@ -0,0 +1,20 @@ +/* +@name sqlStakeDelegationByPool +@param pools -> (...) +*/ +SELECT + encode(credential, 'hex') as credential, + encode("Transaction".hash, 'hex') as tx_id, + COALESCE("StakeDelegationCredentialRelation".pool_credential IN :pools!, false) as is_delegation +FROM "StakeDelegationCredentialRelation" +JOIN "StakeCredential" ON stake_credential = "StakeCredential".id +JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id +JOIN "Block" ON "Transaction".block_id = "Block".id +WHERE + ( + "StakeDelegationCredentialRelation".pool_credential IN :pools! OR + "StakeDelegationCredentialRelation".previous_pool IN :pools! + ) AND + "Block".slot > :min_slot! AND + "Block".slot <= :max_slot! +ORDER BY ("Block".height, "Transaction".tx_index) ASC; \ No newline at end of file diff --git a/webserver/server/app/services/Delegation.ts b/webserver/server/app/services/Delegation.ts deleted file mode 100644 index 7b05fb8b..00000000 --- a/webserver/server/app/services/Delegation.ts +++ /dev/null @@ -1,12 +0,0 @@ - -import type { PoolClient } from 'pg'; -import { ISqlStakeDelegationResult, sqlStakeDelegation } from '../models/delegation/delegationForAddress.queries'; - - -export async function delegationForAddress(request: { - address: Buffer, - until: { absoluteSlot: number }, - dbTx: PoolClient, -}): Promise { - return (await sqlStakeDelegation.run({ credential: request.address, slot: request.until.absoluteSlot }, request.dbTx))[0]; -} \ No newline at end of file diff --git a/webserver/server/app/services/DelegationForAddress.ts b/webserver/server/app/services/DelegationForAddress.ts new file mode 100644 index 00000000..e4a9db2f --- /dev/null +++ b/webserver/server/app/services/DelegationForAddress.ts @@ -0,0 +1,10 @@ +import type { PoolClient } from 'pg'; +import { ISqlStakeDelegationForAddressResult, sqlStakeDelegationForAddress } from '../models/delegation/delegationForAddress.queries'; + +export async function delegationForAddress(request: { + address: Buffer, + until: { absoluteSlot: number }, + dbTx: PoolClient, +}): Promise { + return (await sqlStakeDelegationForAddress.run({ credential: request.address, slot: request.until.absoluteSlot }, request.dbTx))[0]; +} \ No newline at end of file diff --git a/webserver/server/app/services/DelegationForPool.ts b/webserver/server/app/services/DelegationForPool.ts new file mode 100644 index 00000000..fa43dd03 --- /dev/null +++ b/webserver/server/app/services/DelegationForPool.ts @@ -0,0 +1,15 @@ +import type { PoolClient } from 'pg'; +import { ISqlStakeDelegationByPoolResult, sqlStakeDelegationByPool } from '../models/delegation/delegationsForPool.queries'; + + +export async function delegationsForPool(request: { + range: { minSlot: number, maxSlot: number }, + pools: Buffer[], + dbTx: PoolClient, +}): Promise { + return (await sqlStakeDelegationByPool.run({ + min_slot: request.range.minSlot, + max_slot: request.range.maxSlot, + pools: request.pools + }, request.dbTx)); +} \ No newline at end of file diff --git a/webserver/shared/models/DelegationForPool.ts b/webserver/shared/models/DelegationForPool.ts new file mode 100644 index 00000000..fd7f05ac --- /dev/null +++ b/webserver/shared/models/DelegationForPool.ts @@ -0,0 +1,13 @@ +import { Address } from "./Address"; +import { Pool } from "./Pool"; + +export type DelegationForPoolRequest = { + pools: Pool[]; + range: { minSlot: number, maxSlot: number } +}; + +export type DelegationForPoolResponse = { + credential: Address; + isDelegation: boolean, + txId: string | null; +}[]; \ No newline at end of file diff --git a/webserver/shared/models/Pool.ts b/webserver/shared/models/Pool.ts new file mode 100644 index 00000000..be8af262 --- /dev/null +++ b/webserver/shared/models/Pool.ts @@ -0,0 +1,8 @@ +/** + * @pattern [0-9a-fA-F]{56} + * @example "8200581c8baf48931c5187cd59fde553f4e7da2e1a2aa9202ec6e67815cb3f8a" + */ +export type PoolHex = string; + +export type Pool = + | PoolHex \ No newline at end of file diff --git a/webserver/shared/routes.ts b/webserver/shared/routes.ts index eca076ae..68994c64 100644 --- a/webserver/shared/routes.ts +++ b/webserver/shared/routes.ts @@ -23,6 +23,10 @@ import type { DelegationForAddressRequest, DelegationForAddressResponse, } from "./models/DelegationForAddress"; +import type { + DelegationForPoolRequest, + DelegationForPoolResponse, +} from "./models/DelegationForPool"; export enum Routes { transactionHistory = "transaction/history", @@ -35,6 +39,7 @@ export enum Routes { dexSwap = "dex/swap", dexLastPrice = "dex/last-price", delegationForAddress = "delegation/address", + delegationForPool = "delegation/pool", } export type EndpointTypes = { @@ -88,4 +93,9 @@ export type EndpointTypes = { input: DelegationForAddressRequest; response: DelegationForAddressResponse; }; + [Routes.delegationForPool]: { + name: typeof Routes.delegationForPool; + input: DelegationForPoolRequest; + response: DelegationForPoolResponse; + }; }; From 21785b0b53e17b005896080221d4dc900f09b25d Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 24 Oct 2023 00:18:30 -0300 Subject: [PATCH 08/12] add slot and pool fields to the delegation for pool query --- docs/bin/openapi.json | 26 +++++++++++++------ .../DelegationForPoolController.ts | 6 ++--- .../delegation/delegationsForPool.queries.ts | 8 +++--- .../models/delegation/delegationsForPool.sql | 3 ++- webserver/shared/models/DelegationForPool.ts | 5 ++-- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/bin/openapi.json b/docs/bin/openapi.json index 748efc5b..70171b2f 100644 --- a/docs/bin/openapi.json +++ b/docs/bin/openapi.json @@ -323,34 +323,44 @@ ], "type": "object" }, + "PoolHex": { + "type": "string", + "example": "8200581c8baf48931c5187cd59fde553f4e7da2e1a2aa9202ec6e67815cb3f8a", + "pattern": "[0-9a-fA-F]{56}" + }, "DelegationForPoolResponse": { "items": { "properties": { + "slot": { + "type": "number", + "format": "double" + }, "txId": { "type": "string", "nullable": true }, - "isDelegation": { - "type": "boolean" + "pool": { + "allOf": [ + { + "$ref": "#/components/schemas/PoolHex" + } + ], + "nullable": true }, "credential": { "$ref": "#/components/schemas/Address" } }, "required": [ + "slot", "txId", - "isDelegation", + "pool", "credential" ], "type": "object" }, "type": "array" }, - "PoolHex": { - "type": "string", - "example": "8200581c8baf48931c5187cd59fde553f4e7da2e1a2aa9202ec6e67815cb3f8a", - "pattern": "[0-9a-fA-F]{56}" - }, "Pool": { "$ref": "#/components/schemas/PoolHex" }, diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts index 0a569e7d..a2e6b8a5 100644 --- a/webserver/server/app/controllers/DelegationForPoolController.ts +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -3,11 +3,8 @@ 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 { getAddressTypes } from '../models/utils'; import { delegationsForPool } from '../services/DelegationForPool'; import { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; @@ -37,8 +34,9 @@ export class DelegationForPoolController extends Controller { return data.map(data => ({ credential: data.credential as string, - isDelegation: data.is_delegation as boolean, + pool: data.pool, txId: data.tx_id as string, + slot: data.slot, })); }); diff --git a/webserver/server/app/models/delegation/delegationsForPool.queries.ts b/webserver/server/app/models/delegation/delegationsForPool.queries.ts index 75c9f0f9..45e1d7c4 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.queries.ts +++ b/webserver/server/app/models/delegation/delegationsForPool.queries.ts @@ -11,7 +11,8 @@ export interface ISqlStakeDelegationByPoolParams { /** 'SqlStakeDelegationByPool' return type */ export interface ISqlStakeDelegationByPoolResult { credential: string | null; - is_delegation: boolean | null; + pool: string | null; + slot: number; tx_id: string | null; } @@ -21,7 +22,7 @@ export interface ISqlStakeDelegationByPoolQuery { result: ISqlStakeDelegationByPoolResult; } -const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot":true,"max_slot":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":160,"b":166},{"a":505,"b":511},{"a":572,"b":578}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":603,"b":612}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":635,"b":644}]}],"statement":"SELECT \n\tencode(credential, 'hex') as credential,\n\tencode(\"Transaction\".hash, 'hex') as tx_id,\n\tCOALESCE(\"StakeDelegationCredentialRelation\".pool_credential IN :pools!, false) as is_delegation\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n (\n\t\t\"StakeDelegationCredentialRelation\".pool_credential IN :pools! OR\n\t \t\"StakeDelegationCredentialRelation\".previous_pool IN :pools!\n\t) AND\n\t\"Block\".slot > :min_slot! AND\n\t\"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; +const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot":true,"max_slot":true},"params":[{"name":"pools","required":true,"transform":{"type":"array_spread"},"locs":[{"a":176,"b":182},{"a":590,"b":596},{"a":657,"b":663}]},{"name":"min_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":688,"b":697}]},{"name":"max_slot","required":true,"transform":{"type":"scalar"},"locs":[{"a":720,"b":729}]}],"statement":"SELECT \n\tencode(credential, 'hex') as credential,\n\tencode(\"Transaction\".hash, 'hex') as tx_id,\n\t\"Block\".slot,\n\tCASE WHEN \"StakeDelegationCredentialRelation\".pool_credential IN :pools! THEN encode(\"StakeDelegationCredentialRelation\".pool_credential, 'hex') ELSE NULL END AS pool\nFROM \"StakeDelegationCredentialRelation\"\nJOIN \"StakeCredential\" ON stake_credential = \"StakeCredential\".id\nJOIN \"Transaction\" ON \"Transaction\".id = \"StakeDelegationCredentialRelation\".tx_id\nJOIN \"Block\" ON \"Transaction\".block_id = \"Block\".id\nWHERE \n (\n\t\t\"StakeDelegationCredentialRelation\".pool_credential IN :pools! OR\n\t \t\"StakeDelegationCredentialRelation\".previous_pool IN :pools!\n\t) AND\n\t\"Block\".slot > :min_slot! AND\n\t\"Block\".slot <= :max_slot!\nORDER BY (\"Block\".height, \"Transaction\".tx_index) ASC"}; /** * Query generated from SQL: @@ -29,7 +30,8 @@ const sqlStakeDelegationByPoolIR: any = {"usedParamSet":{"pools":true,"min_slot" * SELECT * encode(credential, 'hex') as credential, * encode("Transaction".hash, 'hex') as tx_id, - * COALESCE("StakeDelegationCredentialRelation".pool_credential IN :pools!, false) as is_delegation + * "Block".slot, + * CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') ELSE NULL END AS pool * FROM "StakeDelegationCredentialRelation" * JOIN "StakeCredential" ON stake_credential = "StakeCredential".id * JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id diff --git a/webserver/server/app/models/delegation/delegationsForPool.sql b/webserver/server/app/models/delegation/delegationsForPool.sql index c2ea2a57..2e9379c1 100644 --- a/webserver/server/app/models/delegation/delegationsForPool.sql +++ b/webserver/server/app/models/delegation/delegationsForPool.sql @@ -5,7 +5,8 @@ SELECT encode(credential, 'hex') as credential, encode("Transaction".hash, 'hex') as tx_id, - COALESCE("StakeDelegationCredentialRelation".pool_credential IN :pools!, false) as is_delegation + "Block".slot, + CASE WHEN "StakeDelegationCredentialRelation".pool_credential IN :pools! THEN encode("StakeDelegationCredentialRelation".pool_credential, 'hex') ELSE NULL END AS pool FROM "StakeDelegationCredentialRelation" JOIN "StakeCredential" ON stake_credential = "StakeCredential".id JOIN "Transaction" ON "Transaction".id = "StakeDelegationCredentialRelation".tx_id diff --git a/webserver/shared/models/DelegationForPool.ts b/webserver/shared/models/DelegationForPool.ts index fd7f05ac..37209e58 100644 --- a/webserver/shared/models/DelegationForPool.ts +++ b/webserver/shared/models/DelegationForPool.ts @@ -1,5 +1,5 @@ import { Address } from "./Address"; -import { Pool } from "./Pool"; +import { Pool, PoolHex } from "./Pool"; export type DelegationForPoolRequest = { pools: Pool[]; @@ -8,6 +8,7 @@ export type DelegationForPoolRequest = { export type DelegationForPoolResponse = { credential: Address; - isDelegation: boolean, + pool: PoolHex | null, txId: string | null; + slot: number; }[]; \ No newline at end of file From 02f0174c185fd26d4fde97488acd18e37d0a3aee Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Mon, 6 Nov 2023 23:06:38 -0300 Subject: [PATCH 09/12] delegationForAddress: extract staking credential if arg is full address --- .../DelegationForAddressController.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/webserver/server/app/controllers/DelegationForAddressController.ts b/webserver/server/app/controllers/DelegationForAddressController.ts index 9569b9dd..5bd5f308 100644 --- a/webserver/server/app/controllers/DelegationForAddressController.ts +++ b/webserver/server/app/controllers/DelegationForAddressController.ts @@ -7,9 +7,9 @@ import { genErrorMessage } from '../../../shared/errors'; import { Errors } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; -import { getAddressTypes } from '../models/utils'; import { delegationForAddress } from '../services/DelegationForAddress'; import { DelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; +import { Address } from '@dcspark/cardano-multiplatform-lib-nodejs'; const route = Routes.delegationForAddress; @@ -26,23 +26,39 @@ export class DelegationForAddressController extends Controller { ErrorShape > ): Promise { - const addressTypes = getAddressTypes([requestBody.address]); + const address = Address.from_bech32(requestBody.address); + const rewardAddr = address.as_reward(); + const stakingCred = address.staking_cred(); + + let credential: Buffer; + + if(rewardAddr) { + credential = Buffer.from(rewardAddr.payment_cred().to_bytes()); + rewardAddr.free(); + } + else if(stakingCred) { + credential = Buffer.from(stakingCred.to_bytes()); + stakingCred.free(); + } + else { + address.free(); - if (addressTypes.invalid.length > 0) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return errorResponse( StatusCodes.UNPROCESSABLE_ENTITY, genErrorMessage(Errors.IncorrectAddressFormat, { - addresses: addressTypes.invalid, + addresses: [requestBody.address], }) ); } + address.free(); + const response = await tx< DelegationForAddressResponse >(pool, async dbTx => { const data = await delegationForAddress({ - address: addressTypes.credentialHex.map(addr => Buffer.from(addr, 'hex'))[0], + address: credential, until: requestBody.until, dbTx }); From dc5338bf9d3f2f8a85ec1092a6a9388b5c456b4d Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 7 Nov 2023 13:27:58 -0300 Subject: [PATCH 10/12] rename migration file and add stake credential column index --- indexer/migration/src/lib.rs | 4 ++-- ...20230927_000016_create_stake_delegation_table.rs} | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) rename indexer/migration/src/{m20230927_231206_create_stake_delegation_table.rs => m20230927_000016_create_stake_delegation_table.rs} (86%) diff --git a/indexer/migration/src/lib.rs b/indexer/migration/src/lib.rs index 15d1f8f7..0e08fb31 100644 --- a/indexer/migration/src/lib.rs +++ b/indexer/migration/src/lib.rs @@ -17,7 +17,7 @@ mod m20220528_000012_create_plutus_data_table; mod m20220808_000013_create_transaction_reference_input_table; mod m20221031_000014_create_dex_table; mod m20230223_000015_modify_block_table; -mod m20230927_231206_create_stake_delegation_table; +mod m20230927_000016_create_stake_delegation_table; pub struct Migrator; @@ -42,7 +42,7 @@ impl MigratorTrait for Migrator { Box::new(m20220808_000013_create_transaction_reference_input_table::Migration), Box::new(m20221031_000014_create_dex_table::Migration), Box::new(m20230223_000015_modify_block_table::Migration), - Box::new(m20230927_231206_create_stake_delegation_table::Migration), + Box::new(m20230927_000016_create_stake_delegation_table::Migration), ] } } diff --git a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs b/indexer/migration/src/m20230927_000016_create_stake_delegation_table.rs similarity index 86% rename from indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs rename to indexer/migration/src/m20230927_000016_create_stake_delegation_table.rs index 1f490f8a..2f2d5b73 100644 --- a/indexer/migration/src/m20230927_231206_create_stake_delegation_table.rs +++ b/indexer/migration/src/m20230927_000016_create_stake_delegation_table.rs @@ -6,7 +6,7 @@ pub struct Migration; impl MigrationName for Migration { fn name(&self) -> &str { - "m20230927_231206_create_stake_delegation_table" + "m20230927_000016_create_stake_delegation_table" } } @@ -54,6 +54,16 @@ impl MigrationTrait for Migration { ) .to_owned(), ) + .await?; + + manager + .create_index( + Index::create() + .table(Entity) + .name("index-stake_delegation_credential-stake_credential") + .col(Column::StakeCredential) + .to_owned(), + ) .await } From d474564752af9a28ec9503eef0b828bc1fcb1db9 Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 28 Nov 2023 13:08:37 -0300 Subject: [PATCH 11/12] add limit for slot range and pools --- .../DelegationForPoolController.ts | 80 ++++++++++++------- webserver/shared/constants.ts | 5 ++ webserver/shared/errors.ts | 14 ++++ 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts index a2e6b8a5..696fb6b4 100644 --- a/webserver/server/app/controllers/DelegationForPoolController.ts +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -2,44 +2,66 @@ import { Body, Controller, TsoaResponse, Res, Post, Route, SuccessResponse } fro import { StatusCodes } from 'http-status-codes'; import tx from 'pg-tx'; import pool from '../services/PgPoolSingleton'; -import type { ErrorShape } from '../../../shared/errors'; +import { genErrorMessage, type ErrorShape, Errors } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { delegationsForPool } from '../services/DelegationForPool'; import { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; +import { POOL_DELEGATION_LIMIT } from '../../../shared/constants'; const route = Routes.delegationForPool; @Route('delegation/pool') export class DelegationForPoolController extends Controller { - @SuccessResponse(`${StatusCodes.OK}`) - @Post() - public async delegationForPool( - @Body() - requestBody: EndpointTypes[typeof route]['input'], - @Res() - errorResponse: TsoaResponse< - StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, - ErrorShape - > - ): Promise { - const response = await tx< - DelegationForPoolResponse - >(pool, async dbTx => { - const data = await delegationsForPool({ - pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')), - range: requestBody.range, - dbTx - }); - - return data.map(data => ({ - credential: data.credential as string, - pool: data.pool, - txId: data.tx_id as string, - slot: data.slot, - })); - }); + @SuccessResponse(`${StatusCodes.OK}`) + @Post() + public async delegationForPool( + @Body() + requestBody: EndpointTypes[typeof route]['input'], + @Res() + errorResponse: TsoaResponse< + StatusCodes.BAD_REQUEST | StatusCodes.CONFLICT | StatusCodes.UNPROCESSABLE_ENTITY, + ErrorShape + > + ): Promise { + if (requestBody.pools.length > POOL_DELEGATION_LIMIT.POOLS) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.PoolsLimitExceeded, { + limit: POOL_DELEGATION_LIMIT.POOLS, + found: requestBody.pools.length, + }) + ); + } - return response; + const slotRangeSize = requestBody.range.maxSlot - requestBody.range.minSlot; + if (slotRangeSize > POOL_DELEGATION_LIMIT.SLOT_RANGE) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return errorResponse( + StatusCodes.BAD_REQUEST, + genErrorMessage(Errors.SlotRangeLimitExceeded, { + limit: POOL_DELEGATION_LIMIT.SLOT_RANGE, + found: slotRangeSize, + }) + ); } + + const response = await tx(pool, async dbTx => { + const data = await delegationsForPool({ + pools: requestBody.pools.map(poolId => Buffer.from(poolId, 'hex')), + range: requestBody.range, + dbTx, + }); + + return data.map(data => ({ + credential: data.credential as string, + pool: data.pool, + txId: data.tx_id as string, + slot: data.slot, + })); + }); + + return response; + } } diff --git a/webserver/shared/constants.ts b/webserver/shared/constants.ts index dcc104d3..f5c7c900 100644 --- a/webserver/shared/constants.ts +++ b/webserver/shared/constants.ts @@ -26,3 +26,8 @@ export const DEX_PRICE_LIMIT = { REQUEST_ASSET_PAIRS: 100, RESPONSE: 1000, }; + +export const POOL_DELEGATION_LIMIT = { + POOLS: 50, + SLOT_RANGE: 200, +}; diff --git a/webserver/shared/errors.ts b/webserver/shared/errors.ts index 8953a6cf..d37c09c4 100644 --- a/webserver/shared/errors.ts +++ b/webserver/shared/errors.ts @@ -12,6 +12,8 @@ export enum ErrorCodes { AssetLimitExceeded = 8, CredentialLimitExceeded = 9, AssetPairLimitExceeded = 10, + PoolsLimitExceeded = 11, + SlotRangeLimitExceeded = 12, } export type ErrorShape = { @@ -86,6 +88,18 @@ export const Errors = { detailsGen: (details: { limit: number; found: number }) => `Limit of ${details.limit}, found ${details.found}`, }, + PoolsLimitExceeded: { + code: ErrorCodes.PoolsLimitExceeded, + prefix: "Exceeded request pools limit.", + detailsGen: (details: { limit: number; found: number }) => + `Limit of ${details.limit}, found ${details.found}`, + }, + SlotRangeLimitExceeded: { + code: ErrorCodes.SlotRangeLimitExceeded, + prefix: "Exceeded request slot range limit.", + detailsGen: (details: { limit: number; found: number }) => + `Limit of ${details.limit}, found ${details.found}`, + }, } as const; export function genErrorMessage( From ac1aa29acfcf3892874454e68beb0fec2c2122cb Mon Sep 17 00:00:00 2001 From: Enzo Cioppettini Date: Tue, 28 Nov 2023 13:18:13 -0300 Subject: [PATCH 12/12] eslint fixes --- .../server/app/controllers/DelegationForAddressController.ts | 2 +- .../server/app/controllers/DelegationForPoolController.ts | 2 +- webserver/server/app/services/DelegationForAddress.ts | 3 ++- webserver/server/app/services/DelegationForPool.ts | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/webserver/server/app/controllers/DelegationForAddressController.ts b/webserver/server/app/controllers/DelegationForAddressController.ts index 5bd5f308..b12d3744 100644 --- a/webserver/server/app/controllers/DelegationForAddressController.ts +++ b/webserver/server/app/controllers/DelegationForAddressController.ts @@ -8,7 +8,7 @@ import { Errors } from '../../../shared/errors'; import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { delegationForAddress } from '../services/DelegationForAddress'; -import { DelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; +import type { DelegationForAddressResponse } from '../../../shared/models/DelegationForAddress'; import { Address } from '@dcspark/cardano-multiplatform-lib-nodejs'; const route = Routes.delegationForAddress; diff --git a/webserver/server/app/controllers/DelegationForPoolController.ts b/webserver/server/app/controllers/DelegationForPoolController.ts index 696fb6b4..3419dbcf 100644 --- a/webserver/server/app/controllers/DelegationForPoolController.ts +++ b/webserver/server/app/controllers/DelegationForPoolController.ts @@ -6,7 +6,7 @@ import { genErrorMessage, type ErrorShape, Errors } from '../../../shared/errors import type { EndpointTypes } from '../../../shared/routes'; import { Routes } from '../../../shared/routes'; import { delegationsForPool } from '../services/DelegationForPool'; -import { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; +import type { DelegationForPoolResponse } from '../../../shared/models/DelegationForPool'; import { POOL_DELEGATION_LIMIT } from '../../../shared/constants'; const route = Routes.delegationForPool; diff --git a/webserver/server/app/services/DelegationForAddress.ts b/webserver/server/app/services/DelegationForAddress.ts index e4a9db2f..00bf7805 100644 --- a/webserver/server/app/services/DelegationForAddress.ts +++ b/webserver/server/app/services/DelegationForAddress.ts @@ -1,5 +1,6 @@ import type { PoolClient } from 'pg'; -import { ISqlStakeDelegationForAddressResult, sqlStakeDelegationForAddress } from '../models/delegation/delegationForAddress.queries'; +import type { ISqlStakeDelegationForAddressResult} from '../models/delegation/delegationForAddress.queries'; +import { sqlStakeDelegationForAddress } from '../models/delegation/delegationForAddress.queries'; export async function delegationForAddress(request: { address: Buffer, diff --git a/webserver/server/app/services/DelegationForPool.ts b/webserver/server/app/services/DelegationForPool.ts index fa43dd03..2854016b 100644 --- a/webserver/server/app/services/DelegationForPool.ts +++ b/webserver/server/app/services/DelegationForPool.ts @@ -1,6 +1,6 @@ import type { PoolClient } from 'pg'; -import { ISqlStakeDelegationByPoolResult, sqlStakeDelegationByPool } from '../models/delegation/delegationsForPool.queries'; - +import type { ISqlStakeDelegationByPoolResult} from '../models/delegation/delegationsForPool.queries'; +import { sqlStakeDelegationByPool } from '../models/delegation/delegationsForPool.queries'; export async function delegationsForPool(request: { range: { minSlot: number, maxSlot: number },