Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hive vote #28

Merged
merged 7 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}
return this._hive.broadcast.vote(
options,
Expand Down
16 changes: 8 additions & 8 deletions src/services/api/api.contoller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('ApiController', () => {
await mongod.stop();
});

describe('/POST /api/v1/hive/post_comment', () => {
describe('/POST /v1/hive/post_comment', () => {
it('should post a comment to HIVE blockchain', async () => {
const jwtToken = 'test_jwt_token';
const body = {
Expand All @@ -120,7 +120,7 @@ describe('ApiController', () => {
};

return request(app.getHttpServer())
.post('/api/v1/hive/post_comment')
.post('/v1/hive/post_comment')
.set('Authorization', `Bearer ${jwtToken}`)
.send(body)
.expect(201)
Expand All @@ -135,13 +135,13 @@ describe('ApiController', () => {
});
});

describe('/POST /api/v1/hive/linkaccount', () => {
describe('/POST /v1/hive/linkaccount', () => {
it('should link a Hive account', async () => {
const jwtToken = 'test_jwt_token';
const body = { username: 'test-account' };

return request(app.getHttpServer())
.post('/api/v1/hive/linkaccount')
.post('/v1/hive/linkaccount')
.set('Authorization', `Bearer ${jwtToken}`)
.send(body)
.expect(201)
Expand All @@ -153,12 +153,12 @@ describe('ApiController', () => {
});
});

describe('/GET /api/v1/profile', () => {
describe('/GET /v1/profile', () => {
it('should get the user profile', async () => {
const jwtToken = 'test_jwt_token';

return request(app.getHttpServer())
.get('/api/v1/profile')
.get('/v1/profile')
.set('Authorization', `Bearer ${jwtToken}`)
.expect(200)
.then(response => {
Expand All @@ -172,7 +172,7 @@ describe('ApiController', () => {
});
});

describe('/GET /api/v1/hive/linked-account/list', () => {
describe('/GET /v1/hive/linked-account/list', () => {
it('should list linked accounts', async () => {
const jwtToken = 'test_jwt_token';

Expand All @@ -181,7 +181,7 @@ describe('ApiController', () => {
await linkedAccountsRepository.verify(link._id);

return request(app.getHttpServer())
.get('/api/v1/hive/linked-account/list')
.get('/v1/hive/linked-account/list')
.set('Authorization', `Bearer ${jwtToken}`)
.expect(200)
.then(response => {
Expand Down
58 changes: 28 additions & 30 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('/api/v1')
@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}`);
}

Comment on lines +26 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need username (account) validation as well? If not, I would assume that the above checks are just sanity checks.

This also brings up a bigger question. What do usernames look like for DID accounts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe they're just ceramic IDs. account is just any network dependant unique identifier in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are ceramic IDs? Do you mean did:key:....s?

Also, what about the question about validation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah they're just sanity checks, they allow us to return them types the type for a username is just string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool. I don't think this should block this PR, but it would probably be better for us to have a better understand of what the account portion of the sub look like for each network type.

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();
});
})
});
Loading