Web3 Firebase API Calls From Frontend

If you want to call the Moralis API from your front-end app, don't do this directly. Your Moralis API key should be secured against theft or abuses. Even if your code is obfuscated, almost everyone can extract your key by the HTTP Monitor. This is why you must keep your API key secure on the back-end. In this tutorial we will show you how you can do it for the Firebase.

800800

Firebase Proxy with IP Rate Limiting

At the beginning check our another tutorial: Your First Dapp for Firebase. In this article we won't explain the basic stuff like how to create a project or how to run the Firebase emulator.

In this tutorial we will use 3 Firebase services:

Proxy Function

Basically the idea is quite simple. If you want to call any Moralis API endpoint from your front-end app, you should create a dedicated cloud function for this purpose. This function would be something like a proxy, customised precisely as the front-end needs. In this case, the back-end app knows only the API key.

20582058

Making Moralis API Call by Firebase Function

Let's consider the below function.

import * as functions from 'firebase-functions';

interface GetBlockData {
  chain: string;
  blockNumberOrHash: string;
}

export const getBlock = functions.https.onCall(async (data: GetBlockData) => {
  const response = await Moralis.EvmApi.block.getBlock({
    chain: data.chain,
    blockNumberOrHash: data.blockNumberOrHash,
  });
  return response.toJSON();
});

This cloud function makes a call to the Moralis API and returns a result.

The below code is an example how to call the proxy function from the front-end app. As you can see, the app doesn't pass the API key to the back-end.

const functions = firebase.functions();

const result = await functions.httpsCallable('getBlock')({
  chain: '0x1',
  blockNumberOrHash: '0x9b559aef7ea858608c2e554246fe4a24287e7aeeb976848df2b9a2531f4b9171',
});

One important note: try to keep as little as possible an amount of input parameters. For example, if your product is limited only for the Ethereum blockchain, the chain parameter is redundant. This approach will reduce a chance of an unwanted usage of your functions.

import {EvmChain} from '@moralisweb3/evm-utils';

interface GetBlockData {
  blockNumberOrHash: string;
}

export const getBlock = functions.https.onCall(async (data: GetBlockData) => {
  const response = await Moralis.EvmApi.block.getBlock({
    chain: EvmChain.ETHEREUM,
    blockNumberOrHash: data.blockNumberOrHash,
  });
  return response.toJSON();
});

The same refers to a returning data. If you need only a part of a returned data from the API, return only that what you need. This also will reduce a data transfer.

interface GetBlockTimestampData {
  blockNumberOrHash: string;
}

export const getBlockTimestamp = functions.https.onCall(async (data: GetBlockTimestampData) => {
  const response = await Moralis.EvmApi.block.getBlock({
    chain: '0x1',
    blockNumberOrHash: data.blockNumberOrHash,
  });
  return response.result.result.timestamp.toISOString();
});

Rate Limiting per Client's IP

Another method to secure your functions against abuses is the rate limiting. This technique prevents against a flood of requests (check the DOS). That kind of an attack could block an access to your service and could cause a high billing for dependant services (like the Moralis API).

In this tutorial we will focus on the IP rate limiting. We will implement this feature by using the firebase-functions-rate-limiter package.

The package uses a Firebase database to store an information, how often some qualifier (like a client's IP) requests the service. We can set a limit of requests in a specific period above which the service blocks requests and returns the Too Many Requests HTTP error.

import {FirebaseFunctionsRateLimiter} from 'firebase-functions-rate-limiter';

const firestore = admin.firestore(app);
const limiter = FirebaseFunctionsRateLimiter.withFirestoreBackend(
  {
    name: 'rateLimiter',
    maxCalls: 10,
    periodSeconds: 5,
 },
 firestore);

The package supports both Firebase databases: the RealTime database and the Firestore. We will use the Firestore because it's cheaper.

Now we need to create a qualifier. A qualifier cannot contain dots and colons, so we need to normalise an IP string.

// 1.2.3.4 -> 1-2-3-4
function readNormalizedIp(request: functions.https.Request): string {
  return request.ip ? request.ip.replace(/\.|:/g, '-') : 'unknown';
}

Let's protect our function.

export const getBlock = functions.https.onCall(async (data: GetBlockData) => {
  const qualifier = 'ip-' + readNormalizedIp(context.rawRequest);
  limiter.rejectOnQuotaExceededOrRecordUsage(qualifier);

  // ...
  return response.toJSON();
});

We could finish here, but I want to simplify a usage of the limiter. Let's wrap a whole limiting code into a single function.

import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import {FirebaseFunctionsRateLimiter} from 'firebase-functions-rate-limiter';
import {CallableContext} from 'firebase-functions/v1/https';
import {OnCallHandler} from './OnCallHandler';

export type OnCallHandler<T> = (data: T, context: CallableContext) => Promise<unknown>;

export class IpRateLimiter {
  public constructor(private readonly limiter: FirebaseFunctionsRateLimiter) {}

  public readonly wrap = <T>(handler: OnCallHandler<T>) => {
    return async (data: T, context: CallableContext) => {
      const qualifier = 'ip-' + this.readNormalizedIp(context.rawRequest);

      await this.limiter.rejectOnQuotaExceededOrRecordUsage(qualifier);

      return await handler(data, context);
    };
  };

  private readNormalizedIp(request: functions.https.Request): string {
    return request.ip ? request.ip.replace(/\.|:/g, '-') : 'unknown';
  }
}

export function ipRateLimiterMiddleware(firestore: admin.firestore.Firestore) {
  const limiter = FirebaseFunctionsRateLimiter.withFirestoreBackend(
    {
      name: 'rateLimiter',
      maxCalls: 10,
      periodSeconds: 5,
    },
    firestore,
  );
  return new IpRateLimiter(limiter).wrap;
}

Now we can add the limiting feature to the cloud function in easy way.

const ipRateLimiter = ipRateLimiterMiddleware(firestore);

export const getBlock = functions.https.onCall(ipRateLimiter(async (data: GetBlockData) => {
  const response = await Moralis.EvmApi.block.getBlock(...);
  // ...
}));

Demo Project

You can find the repository with the final code here: firebase-proxy.


Did this page help you?