Skip to content

Commit

Permalink
batch update graffiti api (#1655)
Browse files Browse the repository at this point in the history
* batch update graffiti api

* removing log

* removing log

* Adding more testing to service

* adding test for input length

* fixing test name
  • Loading branch information
patnir authored Oct 5, 2023
1 parent a1bed16 commit 4c96133
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 2 deletions.
152 changes: 151 additions & 1 deletion src/blocks/blocks.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GraphileWorkerService } from '../graphile-worker/graphile-worker.servic
import { PrismaService } from '../prisma/prisma.service';
import { bootstrapTestApp } from '../test/test-app';
import { BlocksService } from './blocks.service';
import { BatchUpdateGraffitiDto } from './dto/batch-update-graffiti.dto';
import { UpsertBlocksDto } from './dto/upsert-blocks.dto';
import { BlockOperation } from './enums/block-operation';
import { SerializedBlockWithTransactions } from './interfaces/serialized-block-with-transactions';
Expand Down Expand Up @@ -208,6 +209,155 @@ describe('BlocksController', () => {
});
});

describe('POST /blocks/batch_update_graffiti', () => {
it('throws error when too many update block objects provided', async () => {
const blockUpdate = {
hash: 'hash',
graffiti:
'676d702f202020202020202020202036356264346432632d6433373237383130',
};
// array with 201 elements
const updates = new Array(201).fill(blockUpdate);

await request(app.getHttpServer())
.post('/blocks/batch_update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
updates,
})
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
});

it('throws error when too many update objects', async () => {
await request(app.getHttpServer())
.post('/blocks/batch_update_graffiti')
.expect(HttpStatus.UNAUTHORIZED);
});

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

await request(app.getHttpServer())
.post('/blocks/batch_update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send([
{
hash: 'hash',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0SSSS',
},
])
.expect(HttpStatus.UNPROCESSABLE_ENTITY);

await request(app.getHttpServer())
.post('/blocks/batch_update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
updates: [
{
hash: 'hash',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0SSSS',
},
],
})
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
});

it('returns the block graffiti', async () => {
const updateGraffiti = jest
.spyOn(blocksService, 'batchUpdateGrafitti')
.mockImplementationOnce(
jest.fn(async (input: BatchUpdateGraffitiDto) => {
const updates = input.updates;

const block1: 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: updates[0].hash,
difficulty: null,
work: null,
sequence: faker.datatype.number(),
timestamp: new Date(),
transactions_count: 0,
graffiti: updates[0].graffiti,
previous_block_hash: uuid(),
size: faker.datatype.number({ min: 1 }),
};

const block2: 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: updates[1].hash,
difficulty: null,
work: null,
sequence: faker.datatype.number(),
timestamp: new Date(),
transactions_count: 0,
graffiti: updates[1].graffiti,
previous_block_hash: uuid(),
size: faker.datatype.number({ min: 1 }),
};

await Promise.resolve([block1, block2]);

return [block1, block2];
}),
);

await request(app.getHttpServer())
.post('/blocks/batch_update_graffiti')
.set('Authorization', `Bearer ${API_KEY}`)
.send({
updates: [
{
hash: 'hash1',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9',
},
{
hash: 'hash2',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e0',
},
],
})
.expect(HttpStatus.CREATED);

expect(updateGraffiti).toHaveBeenCalledWith({
updates: [
{
hash: 'hash1',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9',
},
{
hash: 'hash2',
graffiti:
'a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e9a1b3e4f2c8d0b7e0',
},
],
});
});
});

describe('POST /blocks/update_graffiti', () => {
it('throws unauthorized error when no api key is provided', async () => {
await request(app.getHttpServer())
Expand Down Expand Up @@ -249,7 +399,7 @@ describe('BlocksController', () => {
.expect(HttpStatus.UNPROCESSABLE_ENTITY);
});

it('returns information about the main chain', async () => {
it('returns the block graffiti', async () => {
const updateGraffiti = jest
.spyOn(blocksService, 'updateGraffiti')
.mockImplementationOnce(
Expand Down
25 changes: 24 additions & 1 deletion src/blocks/blocks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ import { PaginatedList } from '../common/interfaces/paginated-list';
import { divide } from '../common/utils/bigint';
import { serializedTransactionFromRecord } from '../transactions/utils/transaction-translator';
import { BlocksService } from './blocks.service';
import { BatchUpdateGraffitiDto } from './dto/batch-update-graffiti.dto';
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 { UpdateGraffitiDto } from './dto/update-graffiti.dto';
import { UpsertBlocksDto } from './dto/upsert-blocks.dto';
import { SerializedBlock } from './interfaces/serialized-block';
import { SerializedBlockHead } from './interfaces/serialized-block-head';
Expand Down Expand Up @@ -80,6 +81,28 @@ export class BlocksController {
return serializedBlockFromRecord(block);
}

@ApiExcludeEndpoint()
@Post('batch_update_graffiti')
@UseGuards(ApiKeyGuard)
async batchUpdateGraffiti(
@Body(
new ValidationPipe({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
transform: true,
}),
)
batchUpdateGraffitiDto: BatchUpdateGraffitiDto,
): Promise<List<SerializedBlock>> {
const blocks = await this.blocksService.batchUpdateGrafitti(
batchUpdateGraffitiDto,
);

return {
object: 'list',
data: blocks.map(serializedBlockFromRecord),
};
}

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

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

await expect(
blocksService.batchUpdateGrafitti({
updates: [{ hash, graffiti }],
}),
).rejects.toThrow(NotFoundException);
});

it('one block in multiple not found', 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);

// does not throw
await expect(
blocksService.batchUpdateGrafitti({
updates: [{ hash: block.hash, graffiti: 'testGraffiti' }],
}),
).resolves.not.toThrow();

await expect(
blocksService.batchUpdateGrafitti({
updates: [
{ hash: block.hash, graffiti: 'testGraffiti' },
{ hash: uuid(), graffiti: 'testGraffiti' },
],
}),
).rejects.toThrow(NotFoundException);
});

it('updates 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.batchUpdateGrafitti({
updates: [{ hash: block.hash, graffiti: 'testGraffiti' }],
});

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

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

describe('updateGraffiti', () => {
it('block not found', async () => {
const graffiti =
Expand Down
45 changes: 45 additions & 0 deletions src/blocks/blocks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { standardizeHash } from '../common/utils/hash';
import { assertValueIsSafeForPrisma } from '../common/utils/prisma';
import { PrismaService } from '../prisma/prisma.service';
import { BasePrismaClient } from '../prisma/types/base-prisma-client';
import { BatchUpdateGraffitiDto } from './dto/batch-update-graffiti.dto';
import { BlockOperation } from './enums/block-operation';
import { BlocksDateMetrics } from './interfaces/blocks-date-metrics';
import { BlocksStatus } from './interfaces/blocks-status';
Expand Down Expand Up @@ -117,6 +118,50 @@ export class BlocksService {
return { ...block, transactions };
}

async batchUpdateGrafitti(input: BatchUpdateGraffitiDto): Promise<Block[]> {
const networkVersion = this.config.get<number>('NETWORK_VERSION');

const blocks = await this.prisma.block.findMany({
where: {
hash: {
in: input.updates.map((update) => standardizeHash(update.hash)),
},
network_version: networkVersion,
},
});

if (blocks.length !== input.updates.length) {
throw new NotFoundException();
}

const hashGraffitiMap = new Map<string, string>();
input.updates.forEach((update) => {
hashGraffitiMap.set(update.hash, update.graffiti);
});

blocks.map((block) => {
const graffiti = hashGraffitiMap.get(block.hash);
if (graffiti) {
block.graffiti = graffiti;
}
});

await this.prisma.$transaction([
...blocks.map((block) =>
this.prisma.block.update({
data: {
graffiti: block.graffiti,
},
where: {
id: block.id,
},
}),
),
]);

return blocks;
}

async updateGraffiti(hash: string, graffiti: string): Promise<Block> {
const networkVersion = this.config.get<number>('NETWORK_VERSION');
const block = await this.prisma.readClient.block.findFirst({
Expand Down
22 changes: 22 additions & 0 deletions src/blocks/dto/batch-update-graffiti.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* 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 { Type } from 'class-transformer';
import {
ArrayMaxSize,
ArrayMinSize,
IsArray,
ValidateNested,
} from 'class-validator';
import { UpdateGraffitiDto } from './update-graffiti.dto';

export class BatchUpdateGraffitiDto {
@IsArray()
@ApiProperty({ description: 'hash + graffiti array' })
@ArrayMinSize(1)
@ArrayMaxSize(200)
@ValidateNested({ each: true })
@Type(() => UpdateGraffitiDto)
readonly updates!: UpdateGraffitiDto[];
}
File renamed without changes.

0 comments on commit 4c96133

Please sign in to comment.