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

add event logs to stf and db #409

Merged
merged 27 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8d07413
add events to stf and db tables
ecioppettini Aug 9, 2024
52d58a1
tsconfig.json fixes?
ecioppettini Aug 9, 2024
4b38573
fix some elint errors
ecioppettini Aug 6, 2024
81e1914
emit mqtt events + refactor the api slightly
ecioppettini Aug 10, 2024
639e636
remove unneed eslint exception
ecioppettini Aug 13, 2024
8fac53f
fix type error in generateAppEvents signature
ecioppettini Aug 13, 2024
93af4c2
typed helpers for app events
ecioppettini Aug 13, 2024
2bae7b4
add imported events to the async api endpoint
ecioppettini Aug 14, 2024
d616a64
add fields validation in get_logs rest endpoint
ecioppettini Aug 14, 2024
1ca48b3
add 0x prefix to the address stf type
ecioppettini Aug 14, 2024
7924edb
add errors to logs endpoint
ecioppettini Aug 15, 2024
7be63ee
add block range limits to the rest endpoint
ecioppettini Aug 15, 2024
6d815f5
lint in server.ts types
ecioppettini Aug 15, 2024
c1ad77a
default to empty events if the file is missing
ecioppettini Aug 16, 2024
d6bdfec
undo tsconfig.json reference removal in batcher
ecioppettini Aug 16, 2024
efc5b64
lift the field number restriction in get_logs and document the return…
ecioppettini Aug 16, 2024
eb526c6
refactor duplicated code in paima-sm
ecioppettini Aug 16, 2024
8634d26
Update packages/paima-sdk/paima-events/src/app-events.ts
ecioppettini Aug 16, 2024
ab5edcb
add default for precompiles if the file is missing
ecioppettini Aug 16, 2024
400008e
make toBlock optional in rest endpoint
ecioppettini Aug 19, 2024
b92aa1b
include signatureHash for appEvents in mqtt path
ecioppettini Aug 19, 2024
34ad38e
add index for topic row in event table
ecioppettini Aug 19, 2024
9b32dee
refactor the types and add some helpers
ecioppettini Aug 20, 2024
9c1a4ae
remove package.json dependency that is in tsconfig
ecioppettini Aug 20, 2024
d8a3069
lowercase field name in index's name
ecioppettini Aug 20, 2024
63bb369
refactor the signature of encodeEventForStf
ecioppettini Aug 22, 2024
3fe80ae
rename idx field to logIndex
ecioppettini Aug 23, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type esbuild from 'esbuild';
export function generateConfig(
apiFolder: string,
stfFolder: string,
precompilesFolder: string
precompilesFolder: string,
eventsFolder: string
): {
config: esbuild.BuildOptions;
outFiles: Record<string, string>;
Expand All @@ -19,6 +20,7 @@ export function generateConfig(
[apiFolder]: 'endpoints.cjs',
[stfFolder]: 'gameCode.cjs',
[precompilesFolder]: 'precompiles.cjs',
[eventsFolder]: 'events.cjs',
};

const config: esbuild.BuildOptions = {
Expand Down
154 changes: 153 additions & 1 deletion packages/engine/paima-rest/src/controllers/BasicControllers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Response, Query, Get, Route } from 'tsoa';
import { Controller, Response, Query, Get, Route, Body, Post } from 'tsoa';
import { doLog, logError, ENV } from '@paima/utils';
import type {
InternalServerErrorResult,
Expand All @@ -7,6 +7,7 @@ import type {
ValidateErrorResult,
} from '@paima/utils';
import { EngineService } from '../EngineService.js';
import type { IGetEventsResult } from '@paima/db';
import {
deploymentChainBlockheightToEmulated,
emulatedSelectLatestPrior,
Expand Down Expand Up @@ -397,3 +398,154 @@ export class TransactionContentController extends Controller {
}
}
}

type GetLogsResponse = Result<
{
topic: string;
address: string;
blockNumber: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: { [fieldName: string]: any };
tx: number;
logIndex: number;
}[]
>;
type GetLogsParams = {
fromBlock: number;
toBlock?: number;
address: string;
filters?: { [fieldName: string]: string };
topic: string;
};

@Route('get_logs')
export class GetLogsController extends Controller {
@Response<FailedResult>(StatusCodes.NOT_FOUND)
@Response<FailedResult>(StatusCodes.BAD_REQUEST)
@Response<InternalServerErrorResult>(StatusCodes.INTERNAL_SERVER_ERROR)
@Post()
public async post(@Body() params: GetLogsParams): Promise<GetLogsResponse> {
const gameStateMachine = EngineService.INSTANCE.getSM();

params.toBlock = params.toBlock ?? (await gameStateMachine.latestProcessedBlockHeight());

if (
params.toBlock < params.fromBlock ||
params.toBlock - params.fromBlock > ENV.GET_LOGS_MAX_BLOCK_RANGE
) {
this.setStatus(StatusCodes.BAD_REQUEST);
return {
success: false,
errorMessage: 'Invalid block range',
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eventDefinition = ((): any => {
const appEvents = gameStateMachine.getAppEvents();

for (const defs of Object.values(appEvents)) {
for (const def of defs) {
if (def.topicHash === params.topic) {
return def;
}
}
}

return undefined;
})();

if (!eventDefinition) {
this.setStatus(StatusCodes.NOT_FOUND);
return {
success: false,
errorMessage: 'Topic not found',
};
}

if (params.filters) {
const indexedFields = new Set();
for (const field of eventDefinition.definition.fields) {
if (field.indexed) {
indexedFields.add(field.name);
}
}

for (const fieldName of Object.keys(params.filters)) {
if (!indexedFields.has(fieldName)) {
this.setStatus(StatusCodes.NOT_FOUND);
return {
success: false,
errorMessage: `Field is not indexed: ${fieldName}`,
};
}
}
}

try {
const DBConn = gameStateMachine.getReadonlyDbConn();

let dynamicPart = '';
const dynamicFilters = [];

// it does not seem to be possible to build this dynamic filter with
// pgtyped, so we instead build a dynamic parametrized query.
if (params.filters) {
const keys = Object.keys(params.filters);

for (let i = 0; i < keys.length; i++) {
dynamicPart = dynamicPart.concat(
`COALESCE(data->>$${5 + i * 2} = $${5 + i * 2 + 1}, 1=1) AND\n`
);

dynamicFilters.push(keys[i]);
dynamicFilters.push(params.filters[keys[i]]);
}
}

const query = `
SELECT * FROM event WHERE
COALESCE(block_height >= $1, 1=1) AND
COALESCE(block_height <= $2, 1=1) AND
COALESCE(address = $3, 1=1) AND
${dynamicPart}
topic = $4
ORDER BY id;
`;

// casting to IGetEventsResult is sound, since both are a select from the
// same table with the same rows, and at least if the table changes there
// is a chance that this will not typecheck anymore.
const rows = (
await DBConn.query(query, [
params.fromBlock,
params.toBlock,
params.address,
params.topic,
...dynamicFilters,
])
).rows as IGetEventsResult[];

return {
success: true,
result: rows.map(row => ({
topic: row.topic,
blockNumber: row.block_height,
address: row.address,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: row.data as { [fieldName: string]: any },
tx: row.tx,
logIndex: row.log_index,
})),
};
} catch (err) {
doLog(`Unexpected webserver error:`);
logError(err);
this.setStatus(StatusCodes.INTERNAL_SERVER_ERROR);
return {
success: false,
errorMessage: 'Unknown error, please contact game node operator',
};
}
}
}
56 changes: 56 additions & 0 deletions packages/engine/paima-rest/src/tsoa/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { TransactionCountController } from './../controllers/BasicControllers';
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { TransactionContentController } from './../controllers/BasicControllers';
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { GetLogsController } from './../controllers/BasicControllers';
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
import { AchievementsController } from './../controllers/AchievementsController';
import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express';

Expand Down Expand Up @@ -168,6 +170,30 @@ const models: TsoaRoute.Models = {
"type": {"dataType":"union","subSchemas":[{"ref":"SuccessfulResult_TransactionContentResponse_"},{"ref":"FailedResult"}],"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"SuccessfulResult__topic-string--address-string--blockNumber-number--data_58___91_fieldName-string_93__58_any_--tx-number--logIndex-number_-Array_": {
"dataType": "refObject",
"properties": {
"success": {"dataType":"enum","enums":[true],"required":true},
"result": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"logIndex":{"dataType":"double","required":true},"tx":{"dataType":"double","required":true},"data":{"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"any"},"required":true},"blockNumber":{"dataType":"double","required":true},"address":{"dataType":"string","required":true},"topic":{"dataType":"string","required":true}}},"required":true},
},
"additionalProperties": false,
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"Result__topic-string--address-string--blockNumber-number--data_58___91_fieldName-string_93__58_any_--tx-number--logIndex-number_-Array_": {
"dataType": "refAlias",
"type": {"dataType":"union","subSchemas":[{"ref":"SuccessfulResult__topic-string--address-string--blockNumber-number--data_58___91_fieldName-string_93__58_any_--tx-number--logIndex-number_-Array_"},{"ref":"FailedResult"}],"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"GetLogsResponse": {
"dataType": "refAlias",
"type": {"ref":"Result__topic-string--address-string--blockNumber-number--data_58___91_fieldName-string_93__58_any_--tx-number--logIndex-number_-Array_","validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"GetLogsParams": {
"dataType": "refAlias",
"type": {"dataType":"nestedObjectLiteral","nestedProperties":{"topic":{"dataType":"string","required":true},"filters":{"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"string"}},"address":{"dataType":"string","required":true},"toBlock":{"dataType":"double"},"fromBlock":{"dataType":"double","required":true}},"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"Achievement": {
"dataType": "refObject",
"properties": {
Expand Down Expand Up @@ -517,6 +543,36 @@ export function RegisterRoutes(app: Router) {
}
});
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
app.post('/get_logs',
...(fetchMiddlewares<RequestHandler>(GetLogsController)),
...(fetchMiddlewares<RequestHandler>(GetLogsController.prototype.post)),

async function GetLogsController_post(request: ExRequest, response: ExResponse, next: any) {
const args: Record<string, TsoaRoute.ParameterSchema> = {
params: {"in":"body","name":"params","required":true,"ref":"GetLogsParams"},
};

// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa

let validatedArgs: any[] = [];
try {
validatedArgs = templateService.getValidatedArgs({ args, request, response });

const controller = new GetLogsController();

await templateService.apiHandler({
methodName: 'post',
controller,
response,
next,
validatedArgs,
successStatus: undefined,
});
} catch (err) {
return next(err);
}
});
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
app.get('/achievements/public/list',
...(fetchMiddlewares<RequestHandler>(AchievementsController)),
...(fetchMiddlewares<RequestHandler>(AchievementsController.prototype.public_list)),
Expand Down
Loading