Skip to main content

Proxy Requests to Figment APIs

This guide will show you how to deploy a proxy using a Cloudflare worker to remove API Keys from your clientside codebase and provide a foundational middleware for further security controls. This guide assumes working knowledge of JavaScript and a Cloudflare account.

Motivation

To make use of Figment APIs, calls must be authenticated using your project's API key. In certain instances you may want to make calls from client side applications. Doing so directly would mean having the API Key in the client codebase and visible during network requests. Using a proxy means we can take API Key out of the client side. In addition having a middleware means we can take further steps such as validating CORS Origins, implementing an allowlist for IPs, throttling requests and more.

Disclaimer

It's important to maintain awareness of the security considerations for the environment in which you're operating. The code example we've provided in the guide below is an example to get you started and not meant for production out of the box.

reminder

Always use Transport Layer Security (TLS), meaning WSS or HTTPS protocols. Traffic sent over WS or HTTP protocols is not secure, regardless of the method used.

Create a Cloudflare Service Worker

Create a Service

  1. From the Workers Overview tab, click the "Create a Service" button.
Cloudflare Dashboard
  1. Give the service a name, then click the "Create service" button.
Create a Service

Add Environment Variables

  1. From the Settings tab the worker, add the necessary environment variables.
Environment Variables

You'll need:

  • API_KEY - Your Figment API Key, click the "Encrypt" button once you've set the value.
  • ALLOWED_ORIGINS - A JSON array of all origins that the service should accept calls from e.g. ["docs.figment.io", null]. Note that providing null will allow the service to accept calls without Origin headers e.g. terminal curl request. To disable this feature remove null from the array.
  • PROXY_HOST - The the URL where the worker is deployed e.g. https://proxy.yourusername.workers.dev. If you have set up a custom domain in Cloudflare, use that instead.
Encrypt the API keyAdd Other Environment Variables

Add Proxy Code to Cloudflare Worker

  1. Click on the "Quick edit" button, then copy and paste the code from the Usage section below into the Cloudflare code editor on the left side of the page.
Quick Edit
  1. Save and Deploy the code explained below to the worker, making any adjustments required by your specific use case.
Save and Deploy

Usage

For any Figment API and network to which you want to send a proxied request, you would need to add the hostname to that value in env.HOSTS in the Worker code. The values for protocols, networks and services in HOSTS can be constructed as follows:

  • The protocol will be either wss or https. Not all networks or API endpoints offer WebSocket support.
  • Check the API Reference table, which shows the APIs supported by each network.
    • The APIs are referred to in the code by the variable services.
  • The keys to use can be taken directly from the Figment docs URL: https://docs.figment.io/api-reference/node-api/ethereum
    • The service is node-api.
    • The network (in this example) is ethereum, so the value would be the hostname of the endpoint to which you are proxying the request.

The highlighted lines indicate where these values go in env.HOSTS:

HOSTS: {
wss: {
"node-api": {
...
ethereum: "ethereum-mainnet-websocket-endpoint.figment.io",
...
},
},
https: {
"node-api": {
...
ethereum: "ethereum-mainnet-jsonrpc-endpoint.figment.io",
...
},
...
}
}

The hostnames for Figment's API endpoints are available via the user dashboard when viewing a protocol. Simply copy and paste the appropriate value from the user dashboard.

important

Remember that the hostname does not include the protocol, wss:// or https://.

Figment Dashboard - Hostnames
important

Code examples provided as samples only and should not be used in production out of the box. Use at your own risk.

note
Click here to view the complete example proxy, ≈200 lines of code.
const env = {
API_KEY: API_KEY,
ALLOWED_ORIGINS: JSON.parse(ALLOWED_ORIGINS),
PROXY_HOST: PROXY_HOST,
ALLOWED_METHODS: ["POST", "GET", "OPTIONS"],
HOSTS: {
wss: {
"node-api": {
arbitrum: "",
bnb: "",
celo: "",
"cosmos-tendermint-rpc": "",
ethereum: "",
fantom: "",
kusama: "",
optimism: "",
"osmosis-tendermint-rpc": "",
polkadot: "",
polygon: "",
solana: "",
},
},
https: {
"node-api": {
arbitrum: "",
"avalanche-c-chain": "",
"avalanche-p-chain": "",
"avalanche-x-chain": "",
bnb: "",
celo: "",
"cosmos-lcd": "",
"cosmos-tendermint-rpc": "",
ethereum: "",
fantom: "",
"kusama-sidecar": "",
"mina-graphql": "",
near: "",
optimism: "",
"osmosis-lcd": "",
"osmosis-tendermint-rpc": "",
"polkadot-sidecar": "",
polygon: "",
solana: "",
},
"rewards-api": {
ethereum: "",
polkadot: "",
solana: "",
near: "",
},
"rewards-rates-api": {
ethereum: "",
polkadot: "",
solana: "",
},
"transaction-search-api": {
avalanche: "",
"near-protocol": "",
},
"staking-api": {
avalanche: "",
ethereum: "",
near: "",
polkadot: "",
solana: "",
},
"staking-api-webhooks": {
avalanche: "",
ethereum: "",
near: "",
polkadot: "",
solana: "",
},
},
},
};

/**
* Builds and returns the URL for the node request
* @param {Request} request - inbount request
* @throws {Response} 500 error if network isn't found
* @returns {URL} of the node endpoint
*/
function buildURL(request) {
const [base, service, network, ...routes] =
request.url.split(/(?<!\/)\/(?!\/)/g);

const protocol =
request.headers.get("Upgrade") === "websocket" ? "wss" : "https";
const services = env.HOSTS[protocol][service];
const url = services ? services[network] : null;
if (!url) {
throw new Response(
`service '${service}' for network '${network}' could not be found`,
{
status: 500,
statusText: `Invalid network`,
}
);
}
return new URL(
`${protocol}://${url}/apikey/${env.API_KEY}/${routes.join("/")}`
);
}

/**
* Ensures the request is authorized to proceed
* @param {Request} request - inbound request
* @throws {Response} 403 or 405 if unauthorized
*/
function ensureAuthorized(request) {
let error;

if (!env.ALLOWED_ORIGINS.includes(request.headers.get("Origin"))) {
error = {
message: `Origin ${request.headers.get("Origin")} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (!env.ALLOWED_METHODS.includes(request.method)) {
error = {
message: `Method ${request.method} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (
request.method !== "OPTIONS" &&
request.headers.get("Authorization") !== env.API_KEY
) {
error = {
message: `Not Authorized ${request.method}!`,
status: 403,
statusText: "Not Allowed",
};
}

if (error) {
throw new Response(error.message, {
status: 403,
statusText: `Not Allowed`,
});
}
}

/**
* Builds and returns preflight response based on request
* @param {Request} request - inbound request
* @returns {Response} with required CORS params
*/
function getPreflightResponse(request) {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Headers":
request.headers.get("Access-Control-Request-Headers") || "",
"Access-Control-Allow-Origin": request.headers.get("Origin") || "",
"Access-Control-Allow-Methods": env.ALLOWED_METHODS.join(","),
"Access-Control-Max-Age": "86400",
},
});
}

/**
* Establishes a websocket connection and returns response containing ws client
* @param {string} url - the figment api url
* @throws {Response} relays status and statusText if connection fails
* @returns {Response} with required websocket client
*/
async function getWebsocketConnection(url) {
const result = await fetch(url, {
headers: {
Upgrade: "websocket",
Authorization: env.API_KEY,
},
});

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101
if (result.status !== 101) {
throw new Response(null, {
status: result.status,
statusText: result.statusText,
});
}

// Accept the websocket connection
const websocket = result.webSocket;
websocket.accept();

// Create a client/server to act as the proxy layer
const clientServerPair = new WebSocketPair();
const [client, server] = Object.values(clientServerPair);

// Tell the Workers runtime to listen for WebSocket data & keep the connection open
server.accept();

// Any messages from the client are forwarded to the DataHub socket
server.addEventListener("message", (event) => {
websocket.send(event.data);
});

// Any messages coming from DataHub are forwarded back to the client
websocket.addEventListener("message", (event) => {
server.send(event.data);
});

return new Response(null, {
status: 101,
webSocket: client,
});
}

/**
* Fetches results from Figment APIs and returns result
* @param {string} url - the figment api url
* @param {Request} request - inbound request
* @returns {Response} relays Figment API node response
*/
async function getRpcResponse(url, request) {
const apiRequest = new Request(url, request);
apiRequest.headers.set("origin", `https://${env.PROXY_HOST}`);

// Create then rebuild a new response from the API result so it's mutable
let response = await fetch(apiRequest);
response = new Response(response.body, response);
response.headers.set(
"Access-Control-Allow-Headers",
request.headers.get("Access-Control-Request-Headers") || ""
);
response.headers.set(
"Access-Control-Allow-Origin",
request.headers.get("Origin") || ""
);
response.headers.set(
"Access-Control-Allow-Methods",
env.ALLOWED_METHODS.join(",")
);
return response;
}

// Main function to process the incoming fetch request
addEventListener("fetch", function handleFetch(event) {
try {
const { request } = event;
const url = buildURL(request);
ensureAuthorized(request);
if (request.method === "OPTIONS") {
event.respondWith(getPreflightResponse(request));
} else if (request.headers.get("Upgrade") === "websocket") {
event.respondWith(getWebsocketConnection(url));
} else {
event.respondWith(getRpcResponse(url, request));
}
} catch (error) {
event.respondWith(error);
}
});

You're all set to make requests to Figment APIs and pass responses back to the client without exposing your API keys! To make calls with the proxy, formulate your URL using <proxy-url>/<service>/<network> followed by any query parameters just as with the Figment API calls.

Here are some examples using the JavaScript fetch API, given the proxy http://api-proxy.username.workers.dev:

Fetch Ethereum gas price from Node API
fetch("http://api-proxy.username.workers.dev/node-api/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_gasPrice",
params: [],
id: 1,
}),
});
Fetch Ethereum rewards for a Validator from Rewards API
fetch("http://api-proxy.username.workers.dev/rewards-api/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
network: "ethereum",
accounts: [
"0x8914b656ad92ffd6ad2920f8f2ad186b0502e4848ad5c5451fea01e42898a2ea07009afb5ca0fd20119da155d745a299",
],
start: 151759,
end: 151760,
}),
});
Fetch Avalanche transactions from Transaction Search API
important

GET requests supply the query parameters in the request URL. There is no request body.

fetch(
"http://api-proxy.username.workers.dev/transaction-search-api/avalanche/transactions?network=avalanche&chain_ids=mainnet&start_height=1921984&address=avax1pvkhyf0y9674p2ps40vmp0a8w427384lpr8zan,avax1rlu93pnalclt7h0dte3evua97lv4fp4qzax5wq",
{
method: "GET",
}
);

Continue with the explanation below, or proceed to the Testing and Troubleshooting section.

Code Explanation

Environment Variables

The env constant contains the values of the Cloudflare environment variables API_KEY, ALLOWED_ORIGINS, PROXY_ORIGIN as well as a list of ALLOWED_METHODS and HOSTS.

It is also useful to define hostnames for the Figment API endpoints to which we would be sending requests, separated by their protocol either WebSocket Secure (wss) or SSL (https).

HOSTS separates wss and https hostnames, and places them into each applicable service. Figment provides access to infrastructure as well as APIs. One such API is the Node API, allowing clients to communicate directly with Proof-of-Stake networks.

Thus, env.HOSTS['wss']['node-api'][0] would refer to the Arbitrum hostname, which is where clients send their requests to the Arbitrum Node API.

Click here to view the code.
Note: ... indicates where values were removed for display
const env = {
API_KEY: API_KEY,
ALLOWED_ORIGINS: JSON.parse(ALLOWED_ORIGINS),
PROXY_HOST: PROXY_HOST,
ALLOWED_METHODS: ["POST", "GET", "OPTIONS"],
HOSTS: {
"wss": {
"node-api": {
"arbitrum": "arbitrum-mainnet-ws-archive-dbgtxn.datahub.figment.io",
...
}
...
},
"https": {
"node-api": {
"arbitrum": "arbitrum-mainnet-rpc-archive-dbgtxn.datahub.figment.io",
...
}
...
},
},
};

Building a Request URL

buildURL processes an inbound request, returning the completed URL to be queried. This will be the Figment API endpoint, including the route.

  • protocol in this scope will be wss or https. Based on the presence of an Upgrade header, we know if the incoming connection is requesting to be upgraded to a WebSocket. Each protocol is handled separately.
  • services in this scope will be one of the defined keys in HOSTS, assigned to either protocol ex. node-api
  • routes are passed via the request URL, they indicate which Figment API endpoint to query. For example, /node-api/solana would direct the request to the HOSTS value for that service and network.
Click here to view the code.
/**
* Builds and returns the URL for the node request
* @param {Request} request - inbount request
* @throws {Response} 500 error if network isn't found
* @returns {URL} of the node endpoint
*/
function buildURL(request) {
const [base, service, network, ...routes] =
request.url.split(/(?<!\/)\/(?!\/)/g);

const protocol =
request.headers.get("Upgrade") === "websocket" ? "wss" : "https";
const services = env.HOSTS[protocol][service];
const url = services ? services[network] : null;
if (!url) {
throw new Response(
`service '${service}' for network '${network}' could not be found`,
{
status: 500,
statusText: `Invalid network`,
}
);
}
return new URL(
`${protocol}://${url}/apikey/${env.API_KEY}/${routes.join("/")}`
);
}

Restricted Access

We want to restrict access to the proxy to Origins and methods which are explicitly allowed. If the request does not meet these criteria, or if there is an error, we will respond to the request with the appropriate 4xx status codes.

Click here to view the code.
/**
* Ensures the request is authorized to proceed
* @param {Request} request - inbound request
* @throws {Response} 403 or 405 if unauthorized
*/
function ensureAuthorized(request) {
let error;

if (!env.ALLOWED_ORIGINS.includes(request.headers.get("Origin"))) {
error = {
message: `Origin ${request.headers.get("Origin")} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (!env.ALLOWED_METHODS.includes(request.method)) {
error = {
message: `Method ${request.method} not allowed`,
status: 405,
statusText: "Not Allowed",
};
} else if (
request.method !== "OPTIONS" &&
request.headers.get("Authorization") !== env.API_KEY
) {
error = {
message: `Not Authorized ${request.method}!`,
status: 403,
statusText: "Not Allowed",
};
}

if (error) {
throw new Response(error.message, {
status: 403,
statusText: `Not Allowed`,
});
}
}

CORS Requests

Handle Cross Origin Resource Sharing (CORS) requests by responding with the appropriate headers.

All CORS preflight requests use the OPTIONS method but not all OPTIONS requests are CORS preflight requests.

What this means in practice is that we must treat all OPTIONS requests as CORS preflight requests.

CORS Resources:

Click here to view the code.
/**
* Builds and returns preflight response based on request
* @param {Request} request - inbound request
* @returns {Response} with required CORS params
*/
function getPreflightResponse(request) {
return new Response(null, {
status: 200,
headers: {
"Access-Control-Allow-Headers":
request.headers.get("Access-Control-Request-Headers") || "",
"Access-Control-Allow-Origin": request.headers.get("Origin") || "",
"Access-Control-Allow-Methods": env.ALLOWED_METHODS.join(","),
"Access-Control-Max-Age": "86400",
},
});
}

Handle WebSocket Connections

getWebsocketConnection handles WebSocket traffic by upgrading the connection, accepting the request, creating a client/server pair, and then addEventListener can pass the messages between the client and server. A successful response must always return status code 101 Switching Protocols. Read more about the status code, WebSockets and WebSocket security in the Reference section at the end of the guide.

Click here to view the code.
/**
* Establishes a websocket connection and returns response containing ws client
* @param {string} url - the figment api url
* @throws {Response} relays status and statusText if connection fails
* @returns {Response} with required websocket client
*/
async function getWebsocketConnection(url) {
const result = await fetch(url, {
headers: {
Upgrade: "websocket",
Authorization: env.API_KEY,
},
});

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101
if (result.status !== 101) {
throw new Response(null, {
status: result.status,
statusText: result.statusText,
});
}

// Accept the websocket connection
const websocket = result.webSocket;
websocket.accept();

// Create a client/server to act as the proxy layer
const clientServerPair = new WebSocketPair();
const [client, server] = Object.values(clientServerPair);

// Tell the Workers runtime to listen for WebSocket data & keep the connection open
server.accept();

// Any messages from the client are forwarded to the DataHub socket
server.addEventListener("message", (event) => {
websocket.send(event.data);
});

// Any messages coming from DataHub are forwarded back to the client
websocket.addEventListener("message", (event) => {
server.send(event.data);
});

return new Response(null, {
status: 101,
webSocket: client,
});
}

Handle HTTP Requests

This function uses the fetch API after setting the Authorization and Origin headers. This is a naive implementation, though it is effective when all you want to do is keep your API key secure. From the client point of view, the API key is never visible.

Click here to view the code.
/**
* Fetches results from Figment APIs and returns result
* @param {string} url - the figment api url
* @param {Request} request - inbound request
* @returns {Response} relays Figment API node response
*/
async function getRpcResponse(url, request) {
const apiRequest = new Request(url, request);
apiRequest.headers.set("origin", `https://${env.PROXY_HOST}`);

// Create then rebuild a new response from the API result so it's mutable
let response = await fetch(apiRequest);
response = new Response(response.body, response);
response.headers.set(
"Access-Control-Allow-Headers",
request.headers.get("Access-Control-Request-Headers") || ""
);
response.headers.set(
"Access-Control-Allow-Origin",
request.headers.get("Origin") || ""
);
response.headers.set(
"Access-Control-Allow-Methods",
env.ALLOWED_METHODS.join(",")
);
return response;
}

addEventListener

info

The addEventListener function defines triggers for a Worker script to execute.

Read more about the fetch API and addEventListener in the Cloudflare documentation.

In this case we want to tie all of the functionality together, ensuring requests are handled with the appropriate function. The try/catch block here is necessary to pass along any errors that occur.

Click here to view the code.
/**
* addEventListener is the Service Worker entrypoint.
* @param {string} - fetch
* @param {callback} - A named function to handle the event
*/
addEventListener("fetch", function handleFetch(event) {
try {
const { request } = event;
const url = buildURL(request);
ensureAuthorized(request);
if (request.method === "OPTIONS") {
event.respondWith(getPreflightResponse(request));
} else if (request.headers.get("Upgrade") === "websocket") {
event.respondWith(getWebsocketConnection(url));
} else {
event.respondWith(getRpcResponse(url, request));
}
} catch (error) {
event.respondWith(error);
}
});

Testing and Troubleshooting

In the Cloudflare worker's quick edit view, you can send various HTTP requests directly to the proxy URL. To verify that the proxy is working with your application:

  1. Ensure that you have set your deployed app's hostname in the ALLOWED_ORIGINS environment variable.
  2. Also ensure the deployed proxy worker's URL is set in the PROXY_ORIGIN environment variable.
  3. Send a request to the proxy in Cloudflare:
    • If you change the HTTP method in the Cloudflare Service Worker to POST, remember to also add a Content-Type header with a value of application/json.
    • To get the current block height of Solana for example, append the service and network (/node-api/solana) to the request URL.
  4. Check the response. Status 200 and the expected response from the requested method indicate that all is working as intended.
Send a Request From CloudflareFigment API Response via Cloudflare Proxy

References

You should now have everything you need in order to successfully implement a proxy as a serverless function. Happy building!