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

Validate signed api name #157

Merged
merged 2 commits into from
Dec 6, 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
18 changes: 18 additions & 0 deletions packages/airnode-feed/src/validation/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,21 @@ test('validates trigger references', async () => {
])
);
});

test('trigger must point to a valid Signed API definition', async () => {
// As a note, having unused Signed API definitions is not an error.
const invalidConfig: Config = {
...config,
signedApis: [{ ...config.signedApis[0]!, name: 'different-name' }],
};

await expect(async () => configSchema.parseAsync(invalidConfig)).rejects.toStrictEqual(
new ZodError([
{
code: 'custom',
message: 'Unable to find signed API with name: localhost',
path: ['triggers', 'signedApiUpdates', 0, 'signedApiName'],
},
])
);
});
67 changes: 50 additions & 17 deletions packages/airnode-feed/src/validation/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ import { z, type SuperRefinement } from 'zod';

import packageJson from '../../package.json';

export type Config = z.infer<typeof configSchema>;
export type Address = z.infer<typeof config.evmAddressSchema>;
export type BeaconId = z.infer<typeof config.evmIdSchema>;
export type TemplateId = z.infer<typeof config.evmIdSchema>;
export type EndpointId = z.infer<typeof config.evmIdSchema>;

export const parameterSchema = z.strictObject({
name: z.string(),
type: z.string(),
value: z.string(),
});

export type Parameter = z.infer<typeof parameterSchema>;

export const templateSchema = z.strictObject({
endpointId: config.evmIdSchema,
parameters: z.array(parameterSchema),
});

export type Template = z.infer<typeof templateSchema>;

export const templatesSchema = z.record(config.evmIdSchema, templateSchema).superRefine((templates, ctx) => {
for (const [templateId, template] of Object.entries(templates)) {
// Verify that config.templates.<templateId> is valid by deriving the hash of the endpointId and parameters
Expand Down Expand Up @@ -53,11 +63,15 @@ export const templatesSchema = z.record(config.evmIdSchema, templateSchema).supe
}
});

export type Templates = z.infer<typeof templatesSchema>;

export const endpointSchema = z.strictObject({
oisTitle: z.string(),
endpointName: z.string(),
});

export type Endpoint = z.infer<typeof endpointSchema>;

export const endpointsSchema = z.record(endpointSchema).superRefine((endpoints, ctx) => {
for (const [endpointId, endpoint] of Object.entries(endpoints)) {
// Verify that config.endpoints.<endpointId> is valid
Expand All @@ -77,6 +91,8 @@ export const endpointsSchema = z.record(endpointSchema).superRefine((endpoints,
}
});

export type Endpoints = z.infer<typeof endpointsSchema>;

export const baseBeaconUpdateSchema = z.strictObject({
deviationThreshold: z.number(),
heartbeatInterval: z.number().int(),
Expand All @@ -88,17 +104,23 @@ export const beaconUpdateSchema = z
})
.merge(baseBeaconUpdateSchema);

export type BeaconUpdate = z.infer<typeof beaconUpdateSchema>;

export const signedApiUpdateSchema = z.strictObject({
signedApiName: z.string(),
templateIds: z.array(config.evmIdSchema),
fetchInterval: z.number(),
updateDelay: z.number(),
});

export type SignedApiUpdate = z.infer<typeof signedApiUpdateSchema>;

export const triggersSchema = z.strictObject({
signedApiUpdates: z.array(signedApiUpdateSchema).nonempty(),
});

export type Triggers = z.infer<typeof triggersSchema>;

const validateTemplatesReferences: SuperRefinement<{ templates: Templates; endpoints: Endpoints }> = (config, ctx) => {
for (const [templateId, template] of Object.entries(config.templates)) {
const endpoint = config.endpoints[template.endpointId];
Expand Down Expand Up @@ -203,6 +225,8 @@ export const signedApiSchema = z.strictObject({
url: z.string().url(),
});

export type SignedApi = z.infer<typeof signedApiSchema>;

export const signedApisSchema = z
.array(signedApiSchema)
.nonempty()
Expand All @@ -219,10 +243,28 @@ export const signedApisSchema = z
}
});

const validateSignedApiReferences: SuperRefinement<{
triggers: Triggers;
signedApis: SignedApi[];
}> = (config, ctx) => {
for (const [index, trigger] of config.triggers.signedApiUpdates.entries()) {
const api = config.signedApis.find((api) => api.name === trigger.signedApiName);
if (!api) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Unable to find signed API with name: ${trigger.signedApiName}`,
path: ['triggers', 'signedApiUpdates', index, 'signedApiName'],
});
}
}
};

export const oisesSchema = z.array(oisSchema);

export const apisCredentialsSchema = z.array(config.apiCredentialsSchema);

export type ApisCredentials = z.infer<typeof apisCredentialsSchema>;

export const nodeSettingsSchema = z.strictObject({
nodeVersion: z.string().refine((version) => version === packageJson.version, 'Invalid node version'),
airnodeWalletMnemonic: z.string().refine((mnemonic) => ethers.utils.isValidMnemonic(mnemonic), 'Invalid mnemonic'),
Expand All @@ -248,41 +290,32 @@ export const configSchema = z
})
.superRefine(validateTemplatesReferences)
.superRefine(validateOisReferences)
.superRefine(validateTriggerReferences);
.superRefine(validateTriggerReferences)
.superRefine(validateSignedApiReferences);

export const encodedValueSchema = z.string().regex(/^0x[\dA-Fa-f]{64}$/);

export const signatureSchema = z.string().regex(/^0x[\dA-Fa-f]{130}$/);

export const signedDataSchema = z.strictObject({
timestamp: z.string(),
encodedValue: encodedValueSchema,
signature: signatureSchema,
});

export type SignedData = z.infer<typeof signedDataSchema>;

export const signedApiPayloadSchema = signedDataSchema.extend({
beaconId: config.evmIdSchema,
airnode: config.evmAddressSchema,
templateId: config.evmIdSchema,
});

export type SignedApiPayload = z.infer<typeof signedApiPayloadSchema>;

export const signedApiBatchPayloadSchema = z.array(signedApiPayloadSchema);

export type SignedApiPayload = z.infer<typeof signedApiPayloadSchema>;
export type SignedApiBatchPayload = z.infer<typeof signedApiBatchPayloadSchema>;
export type Config = z.infer<typeof configSchema>;
export type Template = z.infer<typeof templateSchema>;
export type Templates = z.infer<typeof templatesSchema>;
export type BeaconUpdate = z.infer<typeof beaconUpdateSchema>;
export type SignedApiUpdate = z.infer<typeof signedApiUpdateSchema>;
export type Triggers = z.infer<typeof triggersSchema>;
export type Address = z.infer<typeof config.evmAddressSchema>;
export type BeaconId = z.infer<typeof config.evmIdSchema>;
export type TemplateId = z.infer<typeof config.evmIdSchema>;
export type EndpointId = z.infer<typeof config.evmIdSchema>;
export type SignedData = z.infer<typeof signedDataSchema>;
export type Endpoint = z.infer<typeof endpointSchema>;
export type Endpoints = z.infer<typeof endpointsSchema>;
export type ApisCredentials = z.infer<typeof apisCredentialsSchema>;
export type Parameter = z.infer<typeof parameterSchema>;

export const secretsSchema = z.record(z.string());

Expand Down