diff --git a/.changeset/fuzzy-tomatoes-compete.md b/.changeset/fuzzy-tomatoes-compete.md new file mode 100644 index 0000000000..9e87efd674 --- /dev/null +++ b/.changeset/fuzzy-tomatoes-compete.md @@ -0,0 +1,6 @@ +--- +'@moralisweb3/core': minor +'@moralisweb3/streams': minor +--- + +Added verifySignature utility function diff --git a/demos/moralis-stream/src/hook.ts b/demos/moralis-stream/src/hook.ts index 2b39257761..82f179991d 100644 --- a/demos/moralis-stream/src/hook.ts +++ b/demos/moralis-stream/src/hook.ts @@ -1,12 +1,19 @@ /* eslint-disable no-console */ import express, { NextFunction, Request, Response } from 'express'; -import config from './config'; import fs from 'fs'; -import Web3Utils from 'web3-utils'; +import Moralis from 'moralis'; async function webHook(req: Request, res: Response, next: NextFunction) { try { - verifySignature(req); + // Verify request is from Moralis + const providedSignature = req.headers['x-signature']; + if (!providedSignature) { + throw new Error('No signature provided'); + } + Moralis.Streams.verifySignature({ + body: req.body, + signature: providedSignature as string, + }); // Handle data writeTofile(req.body); @@ -37,20 +44,6 @@ function writeTofile(data: any) { }); } -function verifySignature(req: Request) { - const providedSignature = req.headers['x-signature']; - if (!providedSignature) { - throw new Error('No signature provided'); - } - const generatedSignature = Web3Utils.sha3(JSON.stringify(req.body) + config.STREAM_SECRET); - if (providedSignature !== generatedSignature) { - throw new Error('Invalid signature'); - } - - // eslint-disable-next-line no-console - console.log('Signature verified'); -} - async function getEvents(req: Request, res: Response, next: NextFunction) { try { fs.readFile('data.json', 'utf8', (err, result) => { diff --git a/demos/moralis-stream/src/stream/streamService.ts b/demos/moralis-stream/src/stream/streamService.ts index 4632a26653..26d0f30527 100644 --- a/demos/moralis-stream/src/stream/streamService.ts +++ b/demos/moralis-stream/src/stream/streamService.ts @@ -64,14 +64,11 @@ export async function updateStream(id: string, options: StreamOptions) { export async function setSettings({ region, - secretKey, }: { - secretKey: string; region: 'us-east-1' | 'us-west-2' | 'eu-central-1' | 'ap-southeast-1'; }) { const result = await Moralis.Streams.setSettings({ region, - secretKey, }); return result.raw; diff --git a/packages/core/src/Error/ErrorCode.ts b/packages/core/src/Error/ErrorCode.ts index c5791acab6..aadc01c237 100644 --- a/packages/core/src/Error/ErrorCode.ts +++ b/packages/core/src/Error/ErrorCode.ts @@ -46,6 +46,7 @@ export enum StreamErrorCode { GENERIC_STREAM_ERROR = 'S0001', INCORRECT_NETWORK = 'S0002', INCORRECT_PARAMETER = 'S0003', + INVALID_SIGNATURE = 'S0004', NOT_IMPLEMENTED = 'S9000', } diff --git a/packages/streams/package.json b/packages/streams/package.json index 762b13b5aa..3ecb0ad579 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -30,6 +30,7 @@ "dependencies": { "@moralisweb3/api-utils": "^2.4.0", "@moralisweb3/core": "^2.4.0", - "@moralisweb3/evm-utils": "^2.4.0" + "@moralisweb3/evm-utils": "^2.4.0", + "web3-utils": "^1.7.5" } } diff --git a/packages/streams/src/MoralisStreams.ts b/packages/streams/src/MoralisStreams.ts index 4ad0ab10e3..7cf9ed848f 100644 --- a/packages/streams/src/MoralisStreams.ts +++ b/packages/streams/src/MoralisStreams.ts @@ -1,10 +1,11 @@ import { readSettings, setSettings } from './resolvers'; -import { Endpoints } from '@moralisweb3/api-utils'; -import { ApiModule, MoralisCore, MoralisCoreProvider } from '@moralisweb3/core'; +import { ApiConfig, Endpoints } from '@moralisweb3/api-utils'; +import { ApiModule, MoralisCore, MoralisCoreProvider, MoralisStreamError, StreamErrorCode } from '@moralisweb3/core'; import { createStream, CreateStreamOptions } from './methods/create'; import { updateStream, UpdateStreamOptions } from './methods/update'; import { deleteStream, DeleteStreamOptions } from './methods/delete'; import { GetStreamsOptions, getStreams } from './methods/getAll'; +import { verifySignature, VerifySignatureOptions } from './utils/verifySignature'; export const BASE_URL = 'https://streams-api.aws-prod-streams-master-1.moralis.io'; @@ -42,4 +43,16 @@ export class MoralisStreams extends ApiModule { private readonly _readSettings = this.endpoints.createFetcher(readSettings); public readonly readSettings = () => this._readSettings({}); + + public readonly verifySignature = (options: VerifySignatureOptions) => { + const apiKey = this.core.config.get(ApiConfig.apiKey); + + if (!apiKey) { + throw new MoralisStreamError({ + code: StreamErrorCode.GENERIC_STREAM_ERROR, + message: 'unable to verify signature without an api key', + }); + } + return verifySignature(options, apiKey); + }; } diff --git a/packages/streams/src/generated/types.ts b/packages/streams/src/generated/types.ts index 49ea773eb8..5b25737ca1 100644 --- a/packages/streams/src/generated/types.ts +++ b/packages/streams/src/generated/types.ts @@ -9,7 +9,7 @@ export interface paths { }; "/settings": { /** Get the settings for the current project based on the project api-key. */ - get: operations["ReadSettings"]; + get: operations["GetSettings"]; /** Set the settings for the current project based on the project api-key. */ post: operations["SetSettings"]; }; @@ -50,10 +50,8 @@ export interface components { | "eu-central-1" | "ap-southeast-1"; SettingsModel: { - /** @description A user-provided secret-key, all the webhooks will be signed and include a x-signature header with the following: sha3(JSON.stringify(body)+secretKey) */ - secretKey: string; /** @description The region from where all the webhooks will be posted for this project */ - region: components["schemas"]["SettingsRegion"]; + region?: components["schemas"]["SettingsRegion"]; }; /** * Format: uuid @@ -61,6 +59,14 @@ export interface components { * See [RFC 4112](https://tools.ietf.org/html/rfc4122) */ UUID: string; + /** + * @description The stream status: + * [active] The Stream is healthy and processing blocks + * [paused] The Stream is paused and is not processing blocks + * [error] The Stream has encountered an error and is not processing blocks + * @enum {string} + */ + StreamsStatus: "active" | "paused" | "error"; /** * @description The abi to parse the log object of the contract * @example {} @@ -90,7 +96,7 @@ export interface components { tokenAddress?: string | null; /** @description The topic0 of the event in hex, required if the type : log */ topic0?: string | null; - /** @description Include or not native transactions defaults to false */ + /** @description Include or not native transactions defaults to false (only applied when type:contract) */ includeNativeTxs?: boolean; abi?: components["schemas"]["StreamsAbi"] | null; filter?: components["schemas"]["StreamsFilter"] | null; @@ -102,6 +108,8 @@ export interface components { type: components["schemas"]["StreamsType"]; /** @description The unique uuid of the stream */ id?: components["schemas"]["UUID"]; + /** @description The status of the stream. */ + status?: components["schemas"]["StreamsStatus"]; }; StreamsResponse: { /** @description Array of project Streams */ @@ -125,7 +133,7 @@ export interface components { tokenAddress?: string | null; /** @description The topic0 of the event in hex, required if the type : log */ topic0?: string | null; - /** @description Include or not native transactions defaults to false */ + /** @description Include or not native transactions defaults to false (only applied when type:contract) */ includeNativeTxs?: boolean; abi?: components["schemas"]["StreamsAbi"] | null; filter?: components["schemas"]["StreamsFilter"] | null; @@ -158,7 +166,7 @@ export interface operations { }; }; /** Get the settings for the current project based on the project api-key. */ - ReadSettings: { + GetSettings: { parameters: {}; responses: { /** Ok */ diff --git a/packages/streams/src/resolvers/readSettings.ts b/packages/streams/src/resolvers/readSettings.ts index e1a48c4ea0..87f4aa2214 100644 --- a/packages/streams/src/resolvers/readSettings.ts +++ b/packages/streams/src/resolvers/readSettings.ts @@ -1,7 +1,7 @@ import { createEndpoint, createEndpointFactory } from '@moralisweb3/api-utils'; import { operations } from '../generated/types'; -const name = 'ReadSettings'; +const name = 'GetSettings'; type Name = typeof name; type ApiResult = operations[Name]['responses']['200']['content']['application/json']; diff --git a/packages/streams/src/resolvers/setSettings.ts b/packages/streams/src/resolvers/setSettings.ts index fb4b51eca3..9981f2f0f1 100644 --- a/packages/streams/src/resolvers/setSettings.ts +++ b/packages/streams/src/resolvers/setSettings.ts @@ -7,7 +7,7 @@ type Name = typeof name; type BodyParams = operations[Name]['requestBody']['content']['application/json']; type ApiParams = BodyParams; const method = 'post'; -const bodyParams = ['secretKey', 'region'] as const; +const bodyParams = ['region'] as const; export const setSettings = createEndpointFactory(() => createEndpoint({ diff --git a/packages/streams/src/utils/verifySignature.ts b/packages/streams/src/utils/verifySignature.ts new file mode 100644 index 0000000000..01b6145dd8 --- /dev/null +++ b/packages/streams/src/utils/verifySignature.ts @@ -0,0 +1,18 @@ +import { MoralisDataFormatted, MoralisStreamError, StreamErrorCode } from '@moralisweb3/core'; +import { sha3 } from 'web3-utils'; + +export interface VerifySignatureOptions { + body: MoralisDataFormatted; + signature: string; +} + +export const verifySignature = ({ body, signature }: VerifySignatureOptions, apiKey: string): boolean => { + const generatedSignature = sha3(JSON.stringify(body) + apiKey); + if(signature !== generatedSignature) { + throw new MoralisStreamError({ + code: StreamErrorCode.INVALID_SIGNATURE, + message: 'signature is not valid', + }); + } + return true; +};