diff --git a/backend/src/ada-holder/ada-holder.controller.ts b/backend/src/ada-holder/ada-holder.controller.ts new file mode 100644 index 0000000..0ff7d56 --- /dev/null +++ b/backend/src/ada-holder/ada-holder.controller.ts @@ -0,0 +1,23 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { AdaHolderService } from './ada-holder.service'; + +@ApiTags('ada-holder') +@Controller('ada-holder') +export class AdaHolderController { + constructor(private adaHolderService: AdaHolderService) {} + + @Get('get-current-delegation/:stakeKey') + @ApiOperation({ summary: 'Get current delegation of a stake key' }) + @ApiQuery({ name: 'stakeKey', type: 'string', required: true }) + async getCurrentDelegation(@Query('stakeKey') stakeKey: string) { + return this.adaHolderService.getCurrentDelegation(stakeKey); + } + + @Get('get-voting-power/:stakeKey') + @ApiOperation({ summary: 'Get voting power of a stake key' }) + @ApiQuery({ name: 'stakeKey', type: 'string', required: true }) + async getVotingPower(@Query('stakeKey') stakeKey: string) { + return this.adaHolderService.getVotingPower(stakeKey); + } +} diff --git a/backend/src/ada-holder/ada-holder.module.ts b/backend/src/ada-holder/ada-holder.module.ts new file mode 100644 index 0000000..db1f41c --- /dev/null +++ b/backend/src/ada-holder/ada-holder.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AdaHolderController } from './ada-holder.controller'; +import { AdaHolderService } from './ada-holder.service'; + +@Module({ + controllers: [AdaHolderController], + providers: [AdaHolderService], +}) +export class AdaHolderModule {} diff --git a/backend/src/ada-holder/ada-holder.service.ts b/backend/src/ada-holder/ada-holder.service.ts new file mode 100644 index 0000000..d37f6a2 --- /dev/null +++ b/backend/src/ada-holder/ada-holder.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import path from 'path'; +import * as fs from 'fs'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class AdaHolderService { + constructor( + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + async getCurrentDelegation(stakeKey: string) { + try { + const sqlFilePath = path.join( + __dirname, + '../sql', + 'get-current-delegation.sql', + ); + const sql = fs.readFileSync(sqlFilePath, 'utf8'); + + const result = await this.dataSource.query(sql, [stakeKey]); + if (result.length === 0) { + return null; + } + + const [mDRepHash, dRepView, txHash] = result[0]; + + return { + dRepHash: mDRepHash, + dRepView, + txHash, + }; + } catch (error) { + console.error(error); + return null; + } + } + + async getVotingPower(stakeKey: string) { + console.log('stakeKey', stakeKey); + try { + const sqlFilePath = path.join( + __dirname, + '../sql', + 'get-voting-power.sql', + ); + const sql = fs.readFileSync(sqlFilePath, 'utf8'); + + const result = await this.dataSource.query(sql, [stakeKey]); + + if (result.length === 0) { + return 0; + } + + return result[0]; + } catch (error) { + console.error(error); + return null; + } + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 58e3425..2ac2c42 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -3,6 +3,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ProposalModule } from './proposal/proposal.module'; +import { AdaHolderModule } from './ada-holder/ada-holder.module'; +import { DrepModule } from './drep/drep.module'; @Module({ imports: [ @@ -30,6 +32,8 @@ import { ProposalModule } from './proposal/proposal.module'; }), }), ProposalModule, + AdaHolderModule, + DrepModule, ], controllers: [], providers: [], diff --git a/backend/src/drep/drep.controller.ts b/backend/src/drep/drep.controller.ts new file mode 100644 index 0000000..f53929a --- /dev/null +++ b/backend/src/drep/drep.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { DrepService } from './drep.service'; + +@ApiTags('drep') +@Controller('drep') +export class DrepController { + constructor(private drepService: DrepService) {} + + @Get('get-voting-power/:drepId') + @ApiOperation({ summary: 'Get voting power of a drep id' }) + @ApiQuery({ name: 'drepId', type: 'string', required: true }) + async getVotingPower(@Query('drepId') drepId: string) { + return this.drepService.getVotingPower(drepId); + } +} diff --git a/backend/src/drep/drep.module.ts b/backend/src/drep/drep.module.ts new file mode 100644 index 0000000..214714c --- /dev/null +++ b/backend/src/drep/drep.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DrepController } from './drep.controller'; +import { DrepService } from './drep.service'; + +@Module({ + controllers: [DrepController], + providers: [DrepService], +}) +export class DrepModule {} diff --git a/backend/src/drep/drep.service.ts b/backend/src/drep/drep.service.ts new file mode 100644 index 0000000..f86f186 --- /dev/null +++ b/backend/src/drep/drep.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import path from 'path'; +import * as fs from 'fs'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class DrepService { + constructor( + @InjectDataSource() + private readonly dataSource: DataSource, + ) {} + + async getVotingPower(drepId: string) { + try { + const sqlFilePath = path.join( + __dirname, + '../sql', + 'get-voting-power.sql', + ); + const sql = fs.readFileSync(sqlFilePath, 'utf8'); + + const result = await this.dataSource.query(sql, [drepId]); + + if (result.length === 0) { + return 0; + } + + return result[0]; + } catch (error) { + console.error(error); + return null; + } + } +} diff --git a/backend/src/sql/get-current-delegation.sql b/backend/src/sql/get-current-delegation.sql new file mode 100644 index 0000000..8373fbf --- /dev/null +++ b/backend/src/sql/get-current-delegation.sql @@ -0,0 +1,19 @@ +SELECT + CASE + WHEN drep_hash.raw IS NULL THEN NULL + ELSE encode(drep_hash.raw, 'hex') + END AS drep_raw, + drep_hash.view AS drep_view, + encode(tx.hash, 'hex') +FROM delegation_vote +JOIN tx ON tx.id = delegation_vote.tx_id +JOIN drep_hash ON drep_hash.id = delegation_vote.drep_hash_id +JOIN stake_address ON stake_address.id = delegation_vote.addr_id +WHERE stake_address.hash_raw = decode($1, 'hex') + AND NOT EXISTS ( + SELECT * + FROM delegation_vote AS dv2 + WHERE dv2.addr_id = delegation_vote.addr_id + AND dv2.tx_id > delegation_vote.tx_id + ) +LIMIT 1; diff --git a/backend/src/sql/get-votes.sql b/backend/src/sql/get-votes.sql new file mode 100644 index 0000000..6d5e5de --- /dev/null +++ b/backend/src/sql/get-votes.sql @@ -0,0 +1,19 @@ +SELECT DISTINCT ON (voting_procedure.gov_action_proposal_id, voting_procedure.drep_voter) + voting_procedure.gov_action_proposal_id, + CONCAT(encode(gov_action_tx.hash, 'hex'), '#', gov_action_proposal.index), + encode(drep_hash.raw, 'hex'), + voting_procedure.vote::text, + voting_anchor.url, + encode(voting_anchor.data_hash, 'hex'), + block.epoch_no AS epoch_no, + block.time AS time, + encode(vote_tx.hash, 'hex') AS vote_tx_hash +FROM voting_procedure +JOIN gov_action_proposal ON gov_action_proposal.id = voting_procedure.gov_action_proposal_id +JOIN drep_hash ON drep_hash.id = voting_procedure.drep_voter +LEFT JOIN voting_anchor ON voting_anchor.id = voting_procedure.voting_anchor_id +JOIN tx AS gov_action_tx ON gov_action_tx.id = gov_action_proposal.tx_id +JOIN tx AS vote_tx ON vote_tx.id = voting_procedure.tx_id +JOIN block ON block.id = vote_tx.block_id +WHERE drep_hash.raw = decode($1, 'hex') +ORDER BY voting_procedure.gov_action_proposal_id, voting_procedure.drep_voter, voting_procedure.id DESC diff --git a/backend/src/sql/get-voting-power.sql b/backend/src/sql/get-voting-power.sql new file mode 100644 index 0000000..d36d43f --- /dev/null +++ b/backend/src/sql/get-voting-power.sql @@ -0,0 +1,6 @@ +SELECT COALESCE(drep_distr.amount, 0) AS amount +FROM drep_hash +LEFT JOIN drep_distr ON drep_hash.id = drep_distr.hash_id +WHERE drep_hash.raw = DECODE($1::text, 'hex') +ORDER BY epoch_no DESC +LIMIT 1;