Skip to content

Commit

Permalink
Merge pull request #28 from spknetwork/hive-vote
Browse files Browse the repository at this point in the history
Hive vote
  • Loading branch information
sisygoboom authored Jun 29, 2024
2 parents c8287d6 + 87f8592 commit 18737b7
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 60 deletions.
6 changes: 3 additions & 3 deletions src/repositories/hive-chain/hive-chain.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import hiveJsPackage from '@hiveio/hive-js';
import { AuthorPerm, OperationsArray } from './types';
import {
Expand Down Expand Up @@ -172,11 +172,11 @@ export class HiveChainRepository {
}

async vote(options: { author: string; permlink: string; voter: string; weight: number }) {
if (options.weight < 0 || options.weight > 10_000) {
if (options.weight < -10_000 || options.weight > 10_000) {
this.#logger.error(
`Vote weight was out of bounds: ${options.weight}. Skipping ${options.author}/${options.permlink}`,
);
return;
throw new BadRequestException('Hive vote weight out of bounds. Must be between -10000 and 10000');

Check warning on line 179 in src/repositories/hive-chain/hive-chain.repository.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `'Hive·vote·weight·out·of·bounds.·Must·be·between·-10000·and·10000'` with `⏎········'Hive·vote·weight·out·of·bounds.·Must·be·between·-10000·and·10000',⏎······`
}
return this._hive.broadcast.vote(
options,
Expand Down
56 changes: 27 additions & 29 deletions src/services/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Post,
UseGuards,
Body,
BadRequestException,
HttpException,
HttpStatus,
UseInterceptors,
Expand All @@ -14,7 +13,7 @@ import {
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { v4 as uuid } from 'uuid';
import { RequireHiveVerify, UserDetailsInterceptor } from './utils';
import { UserDetailsInterceptor } from './utils';
import {
ApiBadRequestResponse,
ApiBody,
Expand All @@ -24,22 +23,24 @@ import {
} from '@nestjs/swagger';
import { HiveAccountRepository } from '../../repositories/hive-account/hive-account.repository';
import { UserRepository } from '../../repositories/user/user.repository';
import { HiveChainRepository } from '../../repositories/hive-chain/hive-chain.repository';
import { LinkAccountPostDto } from './dto/LinkAccountPost.dto';
import { VotePostResponseDto } from './dto/VotePostResponse.dto';
import { VotePostDto } from './dto/VotePost.dto';
import { LinkedAccountRepository } from '../../repositories/linked-accounts/linked-account.repository';
import { EmailService } from '../email/email.service';
import { parseAndValidateRequest } from '../auth/auth.utils';
import { HiveService } from '../hive/hive.service';
import { HiveChainRepository } from '../../repositories/hive-chain/hive-chain.repository';

@Controller('/v1')
export class ApiController {
readonly #logger = new Logger();
readonly #logger: Logger = new Logger(ApiController.name);

constructor(
private readonly authService: AuthService,
private readonly hiveAccountRepository: HiveAccountRepository,
private readonly userRepository: UserRepository,
private readonly hiveService: HiveService,
private readonly hiveChainRepository: HiveChainRepository,
//private readonly delegatedAuthorityRepository: DelegatedAuthorityRepository,
private readonly linkedAccountsRepository: LinkedAccountRepository,
Expand Down Expand Up @@ -293,6 +294,15 @@ export class ApiController {
return { ok: true };
}

@ApiHeader({
name: 'Authorization',
description: 'JWT Authorization',
required: true,
schema: {
example:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
},
})
@ApiOperation({
summary: 'Votes on a piece of HIVE content using logged in account',
})
Expand All @@ -301,31 +311,19 @@ export class ApiController {
type: VotePostResponseDto,
})
@UseGuards(AuthGuard('jwt'))
@UseGuards(RequireHiveVerify)
@UseInterceptors(UserDetailsInterceptor)
@Post(`/hive/vote`)
async votePost(@Body() data: VotePostDto) {
const { author, permlink } = data;
// const delegatedAuth = await this.delegatedAuthorityRepository.findOne({
// to: 'threespeak.beta',
// from:
// })
// TODO: get hive username from auth
const delegatedAuth = true;
const voter = 'vaultec';
if (delegatedAuth) {
try {
// console.log(out)
return this.hiveChainRepository.vote({ author, permlink, voter, weight: 500 });
} catch (ex) {
console.log(ex);
console.log(ex.message);
throw new BadRequestException(ex.message);
}
// await appContainer.self
} else {
throw new BadRequestException(`Missing posting autority on HIVE account 'vaultec'`, {
description: 'HIVE_MISSING_POSTING_AUTHORITY',
});
}
async votePost(@Body() data: VotePostDto, @Request() req: any) {
const parsedRequest = parseAndValidateRequest(req, this.#logger);
const { author, permlink, weight, votingAccount } = data;

return await this.hiveService.vote({
sub: parsedRequest.user.sub,
votingAccount,
author,
permlink,
weight,
network: parsedRequest.user.network,
});
}
}
5 changes: 3 additions & 2 deletions src/services/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { HiveAccountModule } from '../../repositories/hive-account/hive-account.
import { HiveChainModule } from '../../repositories/hive-chain/hive-chain.module';
import { EmailModule } from '../email/email.module';
import { LinkedAccountModule } from '../../repositories/linked-accounts/linked-account.module';
import { RequireHiveVerify } from './utils';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HiveModule } from '../hive/hive.module';

@Module({
imports: [
AuthModule,
UserModule,
HiveAccountModule,
HiveChainModule,
HiveModule,
LinkedAccountModule,
EmailModule,
JwtModule.registerAsync({
Expand All @@ -29,6 +30,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
}),
],
controllers: [ApiController],
providers: [RequireHiveVerify],
providers: [],
})
export class ApiModule {}
15 changes: 15 additions & 0 deletions src/services/api/dto/VotePost.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class VotePostDto {
@IsNotEmpty()
@ApiProperty({
default: 'sagarkothari88',
})
author: string;

@IsNotEmpty()
@ApiProperty({
default: 'actifit-sagarkothari88-20230211t122818265z',
})
permlink: string;

@IsNotEmpty()
@ApiProperty({
default: 10000,
})
weight: number;

@IsNotEmpty()
@ApiProperty({
default: 'test',
})
votingAccount: string;
}
Empty file removed src/services/api/middleware.ts
Empty file.
15 changes: 0 additions & 15 deletions src/services/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,6 @@ import { JwtService } from '@nestjs/jwt';
import { Observable } from 'rxjs';
import { User } from '../auth/auth.types';

@Injectable()
export class RequireHiveVerify implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const args = context.getArgs();

const { body } = args[0];
// console.log('RequireHiveVerify guard', {
// body,
// user: args[0].user
// })

return true;
}
}

@Injectable()
export class UserDetailsInterceptor implements NestInterceptor {
constructor(private readonly jwtService: JwtService) {}
Expand Down
30 changes: 29 additions & 1 deletion src/services/auth/auth.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { HttpException, HttpStatus, Logger } from '@nestjs/common';
import { UserRequest, interceptedRequestSchema } from './auth.types';
import {
AccountType,
Network,
UserRequest,
accountTypes,
interceptedRequestSchema,
} from './auth.types';

export function parseAndValidateRequest(request: unknown, logger: Logger): UserRequest {
let parsedRequest: UserRequest;
Expand All @@ -11,3 +17,25 @@ export function parseAndValidateRequest(request: unknown, logger: Logger): UserR
}
return parsedRequest;
}

export function parseSub(sub: string): {
accountType: AccountType;
account: string;
network: Network;
} {
const [accountType, account, network] = sub.split('/');

if (!accountTypes.includes(accountType as AccountType)) {
throw new Error(`Invalid account type: ${accountType}`);
}

if (!network.includes(network as Network)) {
throw new Error(`Invalid network: ${network}`);
}

return {
accountType: accountType as AccountType,
account,
network: network as Network,
};
}
60 changes: 59 additions & 1 deletion src/services/hive/hive.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing';
import { HiveChainModule } from '../../repositories/hive-chain/hive-chain.module';
import { HiveAccountModule } from '../../repositories/hive-account/hive-account.module';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { INestApplication, Module, ValidationPipe } from '@nestjs/common';
import { INestApplication, Module, UnauthorizedException, ValidationPipe } from '@nestjs/common';
import { TestingModule } from '@nestjs/testing';
import crypto from 'crypto';
import { HiveModule } from './hive.module';
Expand Down Expand Up @@ -78,4 +78,62 @@ describe('AuthController', () => {
await expect(hiveService.requestHiveAccount('madeupusername77', sub)).rejects.toThrow('Http Exception');
})
})

describe('Vote on a hive post', () => {
it('Votes on a post when a hive user is logged in and the vote is authorised', async () => {
const sub = 'singleton/sisygoboom/hive';
const response = await hiveService.vote({ votingAccount: 'sisygoboom', sub, network: 'hive', author: 'ned', permlink: 'sa', weight: 10000 })

expect(response).toEqual({
block_num: 123456,
expired: false,
id: "mock_id",
trx_num: 789,
})
});

it('Fails when attempting to vote from a different hive account which has not been linked', async () => {
const sub = 'singleton/username1/hive';

await expect(hiveService.vote({ votingAccount: 'username2', sub, network: 'hive', author: 'ned', permlink: 'sa', weight: 10000 }))
.rejects
.toThrow(UnauthorizedException);
});

it('Votes on a post when a hive user is logged in and attepts to vote from a linked account', async () => {
const sub = 'singleton/username1/hive';
await hiveService.insertCreated('username2', sub);
const response = await hiveService.vote({ votingAccount: 'username2', sub, network: 'hive', author: 'ned', permlink: 'sa', weight: 10000 })

expect(response).toEqual({
block_num: 123456,
expired: false,
id: "mock_id",
trx_num: 789,
})
});

it('Votes on a post when a did user is logged in and attepts to vote from a linked account', async () => {
const sub = 'singleton/username1/did';
await hiveService.insertCreated('username2', sub);
const response = await hiveService.vote({ votingAccount: 'username2', sub, network: 'did', author: 'ned', permlink: 'sa', weight: 10000 })

expect(response).toEqual({
block_num: 123456,
expired: false,
id: "mock_id",
trx_num: 789,
})
});

it('Throws an error when a vote weight is invalid', async () => {
const sub = 'singleton/sisygoboom/hive';
await expect(hiveService.vote({ votingAccount: 'sisygoboom', sub, network: 'hive', author: 'ned', permlink: 'sa', weight: 10001 }))
.rejects
.toThrow();
await expect(hiveService.vote({ votingAccount: 'sisygoboom', sub, network: 'hive', author: 'ned', permlink: 'sa', weight: -10001 }))
.rejects
.toThrow();
});
})
});
47 changes: 44 additions & 3 deletions src/services/hive/hive.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { HttpException, HttpStatus, Injectable, Logger, LoggerService } from '@nestjs/common';
import {
HttpException,
HttpStatus,
Injectable,
Logger,
LoggerService,
UnauthorizedException,
} from '@nestjs/common';
import { HiveChainRepository } from '../../repositories/hive-chain/hive-chain.repository';
import 'dotenv/config';
import { HiveAccountRepository } from '../../repositories/hive-account/hive-account.repository';
import { Network } from '../auth/auth.types';
import { parseSub } from '../auth/auth.utils';

@Injectable()
export class HiveService {
Expand All @@ -14,6 +23,38 @@ export class HiveService {
this.#hiveAccountRepository = hiveAccountRepository;
}

async vote({
votingAccount,
sub,
author,
permlink,
weight,
network,
}: {
votingAccount: string;
sub: string;
author: string;
permlink: string;
weight: number;
network: Network;
}) {
// TODO: investigate how this could be reused on other methods that access accounts onchain
if (network === 'hive' && parseSub(sub).account === votingAccount) {
return this.#hiveRepository.vote({ author, permlink, voter: votingAccount, weight });
}

const delegatedAuth = await this.#hiveAccountRepository.findOneByOwnerIdAndHiveAccountName({
account: votingAccount,
user_id: sub,
});

if (!delegatedAuth) {
throw new UnauthorizedException('You have not verified ownership of the target account');
}

return this.#hiveRepository.vote({ author, permlink, voter: votingAccount, weight });
}

async requestHiveAccount(hiveUsername: string, sub: string) {
const existingDbAcocunt = await this.#hiveAccountRepository.findOneByOwnerId({
user_id: sub,
Expand All @@ -28,7 +69,7 @@ export class HiveService {
);
}

const accountCreation = await this.createAccountWithAuthority(hiveUsername);
const accountCreation = await this.#createAccountWithAuthority(hiveUsername);

console.log(accountCreation);

Expand All @@ -37,7 +78,7 @@ export class HiveService {
return accountCreation;
}

async createAccountWithAuthority(hiveUsername: string) {
async #createAccountWithAuthority(hiveUsername: string) {
if (!process.env.ACCOUNT_CREATOR) {
throw new Error('Please set the ACCOUNT_CREATOR env var');
}
Expand Down
Loading

0 comments on commit 18737b7

Please sign in to comment.