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

Implement various minor fixes #140

Merged
merged 1 commit into from
Nov 30, 2023
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
2 changes: 1 addition & 1 deletion packages/api/config/signed-api.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"delaySeconds": 15
}
],
"allowedAirnodes": ["0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"]
"allowedAirnodes": ["0xbF3137b0a7574563a23a8fC8badC6537F98197CC"]
}
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rm -rf coverage dist",
"dev": "nodemon --ext ts,js,json,env --exec \"pnpm ts-node src/dev-server.ts\"",
"dev": "nodemon --ext ts,js,json,env --exec \"DEV_SERVER_PORT=${DEV_SERVER_PORT:-8090} pnpm ts-node src/dev-server.ts\"",
"docker:build": "docker build --target api --tag api3/signed-api:latest ../../",
"docker:run": "docker run --publish 8090:80 -it --init --volume $(pwd)/config:/app/config --env-file .env --rm api3/signed-api:latest",
"eslint:check": "eslint . --ext .js,.ts --max-warnings 0",
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fetchAndCacheConfig } from './config';
import { logger } from './logger';
import { DEFAULT_PORT, startServer } from './server';

const portSchema = z.number().int().positive();
const portSchema = z.coerce.number().int().positive();
Siegrift marked this conversation as resolved.
Show resolved Hide resolved

const startDevServer = async () => {
const config = await fetchAndCacheConfig();
Expand Down
24 changes: 18 additions & 6 deletions packages/api/src/handlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ethers } from 'ethers';
import { omit } from 'lodash';

import { getMockedConfig } from '../test/fixtures';
Expand Down Expand Up @@ -27,9 +28,11 @@ describe(batchInsertData.name, () => {
expect(result).toStrictEqual({
body: JSON.stringify({
message: 'Unable to recover signer address',
detail:
'signature missing v and recoveryParam (argument="signature", value="0xInvalid", code=INVALID_ARGUMENT, version=bytes/5.7.0)',
extra: invalidData,
context: {
detail:
'signature missing v and recoveryParam (argument="signature", value="0xInvalid", code=INVALID_ARGUMENT, version=bytes/5.7.0)',
signedData: invalidData,
},
}),
headers: {
'access-control-allow-methods': '*',
Expand All @@ -45,12 +48,18 @@ describe(batchInsertData.name, () => {
const config = getMockedConfig();
config.allowedAirnodes = [];
jest.spyOn(configModule, 'getConfig').mockReturnValue(config);
const batchData = [await createSignedData()];
const airnodeWallet = ethers.Wallet.fromMnemonic(
'wear lawsuit design cry express certain knock shrug credit wealth update walk'
);
const batchData = [await createSignedData({ airnodeWallet })];

const result = await batchInsertData(batchData);

expect(result).toStrictEqual({
body: JSON.stringify({ message: 'Unauthorized Airnode address' }),
body: JSON.stringify({
message: 'Unauthorized Airnode address',
context: { airnodeAddress: '0x05E4B3cb2A6875bdD4CCb867B6aA833934EDDCBf' },
}),
headers: {
'access-control-allow-methods': '*',
'access-control-allow-origin': '*',
Expand Down Expand Up @@ -103,7 +112,10 @@ describe(batchInsertData.name, () => {
const result = await batchInsertData(batchData);

expect(result).toStrictEqual({
body: JSON.stringify({ message: 'Request timestamp is too far in the future', extra: batchData[0] }),
body: JSON.stringify({
message: 'Request timestamp is too far in the future',
context: { signedData: batchData[0] },
}),
headers: {
'access-control-allow-methods': '*',
'access-control-allow-origin': '*',
Expand Down
46 changes: 25 additions & 21 deletions packages/api/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ import { generateErrorResponse, isBatchUnique } from './utils';
export const batchInsertData = async (requestBody: unknown): Promise<ApiResponse> => {
const goValidateSchema = await go(async () => batchSignedDataSchema.parseAsync(requestBody));
if (!goValidateSchema.success) {
return generateErrorResponse(
400,
'Invalid request, body must fit schema for batch of signed data',
goValidateSchema.error.message
);
return generateErrorResponse(400, 'Invalid request, body must fit schema for batch of signed data', {
detail: goValidateSchema.error.message,
});
}

// Ensure that the batch of signed that comes from a whitelisted Airnode.
Expand All @@ -31,7 +29,10 @@ export const batchInsertData = async (requestBody: unknown): Promise<ApiResponse
allowedAirnodes !== '*' &&
!goValidateSchema.data.every((signedData) => allowedAirnodes.includes(signedData.airnode))
) {
return generateErrorResponse(403, 'Unauthorized Airnode address');
const disallowedAirnodeAddress = goValidateSchema.data.find(
(signedData) => !allowedAirnodes.includes(signedData.airnode)
)!.airnode;
return generateErrorResponse(403, 'Unauthorized Airnode address', { airnodeAddress: disallowedAirnodeAddress });
}

// Ensure there is at least one signed data to push
Expand All @@ -46,30 +47,31 @@ export const batchInsertData = async (requestBody: unknown): Promise<ApiResponse
// The on-chain contract prevents time drift by making sure the timestamp is at most 1 hour in the future. System
// time drift is less common, but we mirror the contract implementation.
if (Number.parseInt(signedData.timestamp, 10) > Math.floor(Date.now() / 1000) + 60 * 60) {
return generateErrorResponse(400, 'Request timestamp is too far in the future', undefined, signedData);
return generateErrorResponse(400, 'Request timestamp is too far in the future', { signedData });
}

const goRecoverSigner = goSync(() => recoverSignerAddress(signedData));
if (!goRecoverSigner.success) {
return generateErrorResponse(400, 'Unable to recover signer address', goRecoverSigner.error.message, signedData);
return generateErrorResponse(400, 'Unable to recover signer address', {
detail: goRecoverSigner.error.message,
signedData,
});
}

if (signedData.airnode !== goRecoverSigner.data) {
return generateErrorResponse(400, 'Signature is invalid', undefined, signedData);
return generateErrorResponse(400, 'Signature is invalid', { signedData });
}

const goDeriveBeaconId = goSync(() => deriveBeaconId(signedData.airnode, signedData.templateId));
if (!goDeriveBeaconId.success) {
return generateErrorResponse(
400,
'Unable to derive beaconId by given airnode and templateId',
goDeriveBeaconId.error.message,
signedData
);
return generateErrorResponse(400, 'Unable to derive beaconId by given airnode and templateId', {
detail: goDeriveBeaconId.error.message,
signedData,
});
}

if (signedData.beaconId !== goDeriveBeaconId.data) {
return generateErrorResponse(400, 'beaconId is invalid', undefined, signedData);
return generateErrorResponse(400, 'beaconId is invalid', { signedData });
}

return null;
Expand All @@ -96,15 +98,17 @@ export const batchInsertData = async (requestBody: unknown): Promise<ApiResponse
// Write batch of validated data to the database
const goBatchWriteDb = await go(async () => putAll(newSignedData));
if (!goBatchWriteDb.success) {
return generateErrorResponse(500, 'Unable to send batch of signed data to database', goBatchWriteDb.error.message);
return generateErrorResponse(500, 'Unable to send batch of signed data to database', {
detail: goBatchWriteDb.error.message,
});
}

// Prune the cache with the data that is too old (no endpoint will ever return it)
const maxDelay = endpoints.reduce((acc, endpoint) => Math.max(acc, endpoint.delaySeconds), 0);
const maxIgnoreAfterTimestamp = Math.floor(Date.now() / 1000 - maxDelay);
const goPruneCache = await go(async () => prune(newSignedData, maxIgnoreAfterTimestamp));
if (!goPruneCache.success) {
return generateErrorResponse(500, 'Unable to remove outdated cache data', goPruneCache.error.message);
return generateErrorResponse(500, 'Unable to remove outdated cache data', { detail: goPruneCache.error.message });
}

return {
Expand All @@ -130,13 +134,13 @@ export const getData = async (airnodeAddress: string, delaySeconds: number): Pro

const { allowedAirnodes } = getConfig();
if (allowedAirnodes !== '*' && !allowedAirnodes.includes(airnodeAddress)) {
return generateErrorResponse(403, 'Unauthorized Airnode address');
return generateErrorResponse(403, 'Unauthorized Airnode address', { airnodeAddress });
}

const ignoreAfterTimestamp = Math.floor(Date.now() / 1000 - delaySeconds);
const goReadDb = await go(async () => getAll(airnodeAddress, ignoreAfterTimestamp));
if (!goReadDb.success) {
return generateErrorResponse(500, 'Unable to get signed data from database', goReadDb.error.message);
return generateErrorResponse(500, 'Unable to get signed data from database', { detail: goReadDb.error.message });
}

const data = goReadDb.data.reduce((acc, signedData) => {
Expand All @@ -156,7 +160,7 @@ export const getData = async (airnodeAddress: string, delaySeconds: number): Pro
export const listAirnodeAddresses = async (): Promise<ApiResponse> => {
const goAirnodeAddresses = await go(async () => getAllAirnodeAddresses());
if (!goAirnodeAddresses.success) {
return generateErrorResponse(500, 'Unable to scan database', goAirnodeAddresses.error.message);
return generateErrorResponse(500, 'Unable to scan database', { detail: goAirnodeAddresses.error.message });
}
const airnodeAddresses = goAirnodeAddresses.data;

Expand Down
9 changes: 6 additions & 3 deletions packages/api/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ export const isIgnored = (signedData: SignedData, ignoreAfterTimestamp: number)
export const generateErrorResponse = (
statusCode: number,
message: string,
detail?: string,
extra?: unknown
context?: Record<string, unknown>
Siegrift marked this conversation as resolved.
Show resolved Hide resolved
): ApiResponse => {
return { statusCode, headers: createResponseHeaders(), body: JSON.stringify({ message, detail, extra }) };
return {
statusCode,
headers: createResponseHeaders(),
body: JSON.stringify(context ? { message, context } : { message }),
};
};