Skip to content

Commit

Permalink
Rahul/ifl 1733 add an endpoint to update block graffiti in syncer (#1654
Browse files Browse the repository at this point in the history
)

* Adding block service function and test

* adding tests for block controller

* using dto object and updating tests

* removing console log

* udpating error message

* Removing regex global
  • Loading branch information
patnir authored Oct 4, 2023
1 parent 4847307 commit a1bed16
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 1 deletion.
87 changes: 87 additions & 0 deletions src/blocks/blocks.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,93 @@ describe('BlocksController', () => {
});
});

describe('POST /blocks/update_graffiti', () => {
it('throws unauthorized error when no api key is provided', async () => {
await request(app.getHttpServer())
.post('/blocks/update_graffiti')
.expect(HttpStatus.UNAUTHORIZED);
});

it('throw error when graffiti is not formatted correctly', async () => {
// graffiti is not hex
await request(app.getHttpServer())
.post('/blocks/update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
hash: 'hash',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0SSSS',
})
.expect(HttpStatus.UNPROCESSABLE_ENTITY);

// graffiti is not 64 characters long
await request(app.getHttpServer())
.post('/blocks/update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
hash: 'hash',
graffiti: 'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c',
})
.expect(HttpStatus.UNPROCESSABLE_ENTITY);

// graffiti is not 64 characters long
await request(app.getHttpServer())
.post('/blocks/update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
hash: 'hash',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2ca1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2ca1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c',
})
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
});

it('returns information about the main chain', async () => {
const updateGraffiti = jest
.spyOn(blocksService, 'updateGraffiti')
.mockImplementationOnce(
jest.fn(async (hash: string, graffiti: string) => {
const block: Block = {
id: faker.datatype.number(),
created_at: new Date(),
updated_at: new Date(),
main: true,
network_version: 0,
time_since_last_block_ms: faker.datatype.number(),
hash: hash,
difficulty: null,
work: null,
sequence: faker.datatype.number(),
timestamp: new Date(),
transactions_count: 0,
graffiti: graffiti,
previous_block_hash: uuid(),
size: faker.datatype.number({ min: 1 }),
};

await Promise.resolve(block);

return block;
}),
);

await request(app.getHttpServer())
.post('/blocks/update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
hash: 'hash',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9',
})
.expect(HttpStatus.CREATED);

expect(updateGraffiti).toHaveBeenCalledWith(
'hash',
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9',
);
});
});

describe('GET /blocks', () => {
describe('with no query parameters', () => {
it('returns a list of blocks in descending order', async () => {
Expand Down
22 changes: 21 additions & 1 deletion src/blocks/blocks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { BlockQueryDto } from './dto/block-query.dto';
import { BlocksMetricsQueryDto } from './dto/blocks-metrics-query.dto';
import { BlocksQueryDto } from './dto/blocks-query.dto';
import { DisconnectBlocksDto } from './dto/disconnect-blocks.dto';
import { UpdateGraffitiDto } from './dto/update-graffiti';
import { UpsertBlocksDto } from './dto/upsert-blocks.dto';
import { SerializedBlock } from './interfaces/serialized-block';
import { SerializedBlockHead } from './interfaces/serialized-block-head';
Expand All @@ -45,7 +46,7 @@ import {
} from './utils/block-translator';
import { serializedBlockMetricsFromRecord } from './utils/blocks-metrics-translator';
import { serializedBlocksStatusFromRecord } from './utils/blocks-status-translator';
import { Asset, AssetDescription, Transaction } from '.prisma/client';
import { Asset, AssetDescription, Block, Transaction } from '.prisma/client';

const MAX_SUPPORTED_TIME_RANGE_IN_DAYS = 90;

Expand All @@ -60,6 +61,25 @@ export class BlocksController {
private readonly blocksTransactionsLoader: BlocksTransactionsLoader,
) {}

@ApiExcludeEndpoint()
@Post('update_graffiti')
@UseGuards(ApiKeyGuard)
async updateGraffiti(
@Body(
new ValidationPipe({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
transform: true,
}),
)
updateGraffitiDto: UpdateGraffitiDto,
): Promise<SerializedBlock> {
const block: Block = await this.blocksService.updateGraffiti(
updateGraffitiDto.hash,
updateGraffitiDto.graffiti,
);
return serializedBlockFromRecord(block);
}

@ApiExcludeEndpoint()
@Post()
@UseGuards(ApiKeyGuard)
Expand Down
40 changes: 40 additions & 0 deletions src/blocks/blocks.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,46 @@ describe('BlocksService', () => {
});
});

describe('updateGraffiti', () => {
it('block not found', async () => {
const graffiti =
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9';
const hash = uuid();

await expect(
blocksService.updateGraffiti(hash, graffiti),
).rejects.toThrow(NotFoundException);
});

it('update graffiti', async () => {
const block = await blocksService.upsert(prisma, {
hash: uuid(),
sequence: faker.datatype.number(),
difficulty: BigInt(faker.datatype.number()),
work: BigInt(faker.datatype.number()),
timestamp: new Date(),
transactionsCount: 1,
type: BlockOperation.CONNECTED,
graffiti: uuid(),
previousBlockHash: uuid(),
size: faker.datatype.number(),
});

const record = await blocksService.find(block.id);
expect(record).toMatchObject(block);

await blocksService.updateGraffiti(block.hash, 'testGraffiti');

const updatedRecord = await blocksService.find(block.id);

expect(updatedRecord).toMatchObject({
...block,
updated_at: updatedRecord?.updated_at,
graffiti: 'testGraffiti',
});
});
});

describe('miningReward', () => {
it('returns the correct mining reward', () => {
const reward = blocksService.miningReward(733106);
Expand Down
23 changes: 23 additions & 0 deletions src/blocks/blocks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,29 @@ export class BlocksService {
return { ...block, transactions };
}

async updateGraffiti(hash: string, graffiti: string): Promise<Block> {
const networkVersion = this.config.get<number>('NETWORK_VERSION');
const block = await this.prisma.readClient.block.findFirst({
where: {
hash: standardizeHash(hash),
network_version: networkVersion,
},
});

if (!block) {
throw new NotFoundException();
}

return await this.prisma.block.update({
data: {
graffiti,
},
where: {
id: block.id,
},
});
}

miningReward(sequence: number): number {
if (sequence <= 1) {
return 0;
Expand Down
35 changes: 35 additions & 0 deletions src/blocks/dto/update-graffiti.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { ApiProperty } from '@nestjs/swagger';
import {
IsDefined,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';

export class UpdateGraffitiDto {
@ApiProperty({ description: 'Block hash' })
@IsDefined({
message: '"hash" of the block',
})
@IsString()
readonly hash!: string;

@ApiProperty({ description: 'Block graffiti' })
@IsDefined({
message: '"graffiti" of the block',
})
@MinLength(64, {
message: 'must be exactly 64 characters in length',
})
@MaxLength(64, {
message: 'must be exactly 64 characters in length',
})
@Matches(/^[0-9A-Fa-f]+$/, {
message: 'must be 64 length hex string',
})
readonly graffiti!: string;
}

0 comments on commit a1bed16

Please sign in to comment.