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; + }; };