Skip to content

Commit

Permalink
Implement various minor fixes (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
Siegrift authored Nov 30, 2023
1 parent 5ed8f94 commit 900628d
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 33 deletions.
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();

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>
): ApiResponse => {
return { statusCode, headers: createResponseHeaders(), body: JSON.stringify({ message, detail, extra }) };
return {
statusCode,
headers: createResponseHeaders(),
body: JSON.stringify(context ? { message, context } : { message }),
};
};

0 comments on commit 900628d

Please sign in to comment.