From 909374a3ee926430b32cf5ac80560335c7994bba Mon Sep 17 00:00:00 2001
From: geel <102362474+0xGeel@users.noreply.github.com>
Date: Fri, 14 Apr 2023 14:42:02 +0200
Subject: [PATCH] Feature: Add rate limiter to Loopring API requests (#20)
**TL;DR:**
- Introducing a Rate Limiter for requests made to the Loopring API.
- Updated the error messages, and added a to notify
end-users of errors.
**Why?**
- Hitting the API Rate Limit caused a bug for users that connected with
a wallet that contained a lot (800+) NFTs. This caused the Loopring API
requests to return a HTTP 429 error, resulting in not all NFTs being
included in the access verification.
---
package-lock.json | 37 ++++++++++++++++++-
package.json | 8 ++--
.../ConnectedPage/ConnectedPage.tsx | 9 +++--
src/pages/_app.tsx | 15 ++++++++
src/pages/api/getUserNfts.ts | 14 +++++--
src/pages/api/getUserUnlocks.ts | 24 ++++++++----
src/utils/loopring/getAllUserNftIds.ts | 10 ++++-
7 files changed, 96 insertions(+), 21 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index bbba49bec..b98455f27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "loopgate",
- "version": "0.2.0",
+ "version": "0.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "loopgate",
- "version": "0.2.0",
+ "version": "0.2.1",
"license": "BSD-2-Clause",
"dependencies": {
"@heroicons/react": "^2.0.13",
@@ -23,6 +23,7 @@
"@radix-ui/react-tabs": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.3",
"axios": "^1.2.3",
+ "axios-rate-limit": "^1.3.0",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"connectkit": "^1.1.1",
@@ -32,6 +33,7 @@
"pinata-submarine": "^0.1.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hot-toast": "^2.4.0",
"siwe": "^2.1.3",
"tailwind-merge": "^1.9.0",
"tailwindcss-animate": "^1.0.5",
@@ -5525,6 +5527,14 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/axios-rate-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/axios-rate-limit/-/axios-rate-limit-1.3.0.tgz",
+ "integrity": "sha512-cKR5wTbU/CeeyF1xVl5hl6FlYsmzDVqxlN4rGtfO5x7J83UxKDckudsW0yW21/ZJRcO0Qrfm3fUFbhEbWTLayw==",
+ "peerDependencies": {
+ "axios": "*"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -8410,6 +8420,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/goober": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.12.tgz",
+ "integrity": "sha512-yXHAvO08FU1JgTXX6Zn6sYCUFfB/OJSX8HHjDSgerZHZmFKAb08cykp5LBw5QnmyMcZyPRMqkdyHUSSzge788Q==",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -12159,6 +12177,21 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz",
+ "integrity": "sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA==",
+ "dependencies": {
+ "goober": "^2.1.10"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index d05996b94..0b2e848e5 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,11 @@
{
"name": "loopgate",
- "version": "0.2.0",
+ "version": "0.2.1",
"description": "Easily Token-Gate Content using Loopring Layer-2 NFTs and Pinata",
"author": "Geel <@0xGeel>",
"license": "BSD-2-Clause",
- "repository": "https://github.com/0xGeel/loopring-token-gating.git",
- "bugs": "https://github.com/0xGeel/loopring-token-gating/issues",
+ "repository": "https://github.com/0xGeel/loopgate.git",
+ "bugs": "https://github.com/0xGeel/loopgate/issues",
"homepage": "https://loopgate.netlify.app",
"scripts": {
"dev": "next dev",
@@ -30,6 +30,7 @@
"@radix-ui/react-tabs": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.3",
"axios": "^1.2.3",
+ "axios-rate-limit": "^1.3.0",
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"connectkit": "^1.1.1",
@@ -39,6 +40,7 @@
"pinata-submarine": "^0.1.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hot-toast": "^2.4.0",
"siwe": "^2.1.3",
"tailwind-merge": "^1.9.0",
"tailwindcss-animate": "^1.0.5",
diff --git a/src/components/ConnectedPage/ConnectedPage.tsx b/src/components/ConnectedPage/ConnectedPage.tsx
index 9f8367f1c..1cb0ee1d0 100644
--- a/src/components/ConnectedPage/ConnectedPage.tsx
+++ b/src/components/ConnectedPage/ConnectedPage.tsx
@@ -5,6 +5,7 @@ import UnlockLink from "./UnlockLink";
import Spinner from "../Spinner";
import { useAccount } from "wagmi";
import axios from "axios";
+import toast from "react-hot-toast";
const ConnectedPage = () => {
const { address } = useAccount();
@@ -19,15 +20,15 @@ const ConnectedPage = () => {
setIsLoading(false);
})
.catch((error) => {
- console.log("Whoops. Something went wrong." + error);
+ toast.error(error.request.response);
setIsLoading(false);
});
};
// On render: make API calls to determine NFT holdings
- // 1.: GET user's Loopring ID (Loopring API) ✅
- // 2.: GET user's NFTs (Loopring API) ✅
- // 3.: Check config to compare NFTs and unlocks ✅
+ // 1.: GET user's Loopring ID (Loopring API)
+ // 2.: GET user's NFTs (Loopring API)
+ // 3.: Check config to compare NFTs and unlocks
// 4.: GET submarined content (Pinata API)
useEffect(() => {
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 7cf07224a..f894a1807 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -9,6 +9,7 @@ import { siwe } from "../utils/siwe";
import { overrides } from "../styles/ConnectKit/overrides";
import NextHeadBase from "../components/SEO/NextHeadBase";
import { inter, unbounded } from "../components/Fonts/Fonts";
+import { Toaster } from "react-hot-toast";
const App = ({ Component, pageProps }: AppProps) => {
const [mounted, setMounted] = useState(false);
@@ -27,6 +28,20 @@ const App = ({ Component, pageProps }: AppProps) => {
{mounted && }
+
diff --git a/src/pages/api/getUserNfts.ts b/src/pages/api/getUserNfts.ts
index c262ba402..79c9a0342 100644
--- a/src/pages/api/getUserNfts.ts
+++ b/src/pages/api/getUserNfts.ts
@@ -8,7 +8,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!accountId || Array.isArray(accountId[0])) {
// Check if multiple or no Account IDs are specified. If so: early return.
- return res.status(400).json({ error: "No Loopring Account ID specified." });
+ return res
+ .status(400)
+ .send(
+ "Invalid Request: 0x address not provided. Please provide a valid 0x address and try again."
+ );
}
// Call Loopring API to find- and extract all user NFT IDs
@@ -16,9 +20,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return allNftIds
? res.status(200).json(allNftIds)
- : res.status(400).json({
- error: "Unable to find any NFTs for the supplied Loopring Account ID",
- });
+ : res
+ .status(400)
+ .send(
+ "Invalid Request: Unable to find any NFTs for the specified 0x address."
+ );
};
export default handler;
diff --git a/src/pages/api/getUserUnlocks.ts b/src/pages/api/getUserUnlocks.ts
index ad0562772..6022f7ab0 100644
--- a/src/pages/api/getUserUnlocks.ts
+++ b/src/pages/api/getUserUnlocks.ts
@@ -16,21 +16,31 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const siweSesh = await siwe.getSession(req, res);
if (!address || Array.isArray(address)) {
- return res.status(400).json({ error: "No 0x address specified." });
+ return res
+ .status(400)
+ .send(
+ "0x address not provided. Please provide a valid 0x address and try again."
+ );
}
// Check if there is a session. Only connected users may call this endpoint.
if (!siweSesh.address || siweSesh.address !== address) {
- return res.status(405).send({ message: "What are ye doin' in my swamp?!" });
+ return res
+ .status(401)
+ .send(
+ "You are not authorized to access this resource. Sign In With Ethereum, and try again."
+ );
}
// 1️⃣ Call the Loopring API to find the User's Loopring Account ID
const accountId = await getUserAddress(address);
if (!accountId) {
- return res.status(400).json({
- error: "Could not find Loopring Account for the specified 0x address",
- });
+ return res
+ .status(400)
+ .send(
+ "No Loopring Account could be found for the connected 0x address. Is your L2 account activated?"
+ );
}
// 2️⃣ Call the Loopring API to find the NFTs held by the user
@@ -38,8 +48,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (!allNftIds) {
return res
- .status(400)
- .json({ error: "Unable to find any NFTs for the specified 0x address" });
+ .status(404)
+ .send("Unable to find any NFTs for the specified 0x address.");
}
// 3️⃣ Check the user's NFT IDs against the config.ts to determine unlocks
diff --git a/src/utils/loopring/getAllUserNftIds.ts b/src/utils/loopring/getAllUserNftIds.ts
index 0c9d0e07e..e02aad5df 100644
--- a/src/utils/loopring/getAllUserNftIds.ts
+++ b/src/utils/loopring/getAllUserNftIds.ts
@@ -1,7 +1,15 @@
import axios from "axios";
+import rateLimit from "axios-rate-limit";
import { API } from "./_constants";
import { headerOpts, extractNfts } from "./index";
+// Loopring API accepts 5 requests per second, max.
+// Although this seems to fluctuate. 20 / sec
+const rateLimitedAxios = rateLimit(axios.create(), {
+ maxRequests: 10,
+ perMilliseconds: 1000,
+});
+
const getAllUserNftIds = async (accountId: string | string[]) => {
// Gets all Loopring L2 NFTs for a specified Account ID
const LIMIT = 50; // API can handle up to 50 per call
@@ -32,7 +40,7 @@ const getAllUserNftIds = async (accountId: string | string[]) => {
// Call the API for all of these ^
const followUpReqs = await Promise.all(
amountOfCalls.map(async (index) => {
- return await axios.get(
+ return await rateLimitedAxios.get(
`${API.USER_NFT_BALANCE}?accountId=${accountId}&limit=${LIMIT}&offset=${
LIMIT * index
}`,