Skip to content

Commit

Permalink
Feature: Add rate limiter to Loopring API requests (#20)
Browse files Browse the repository at this point in the history
**TL;DR:**
- Introducing a Rate Limiter for requests made to the Loopring API. 
- Updated the error messages, and added a <Toaster /> 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.
  • Loading branch information
0xGeel authored Apr 14, 2023
1 parent 8e45b23 commit 909374a
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 21 deletions.
37 changes: 35 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
9 changes: 5 additions & 4 deletions src/components/ConnectedPage/ConnectedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(() => {
Expand Down
15 changes: 15 additions & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -27,6 +28,20 @@ const App = ({ Component, pageProps }: AppProps) => {
<main className={`${inter.variable} ${unbounded.variable} font-sans`}>
<NextHeadBase />
{mounted && <Component {...pageProps} />}
<Toaster
toastOptions={{
style: {
backgroundColor: "rgba(17,24,44,.8)",
backdropFilter: "blur(2px)",
color: "#FFFFFF",
fontSize: "14px",
border: "1px solid rgba(255,255,255,.2)",
maxWidth: "420px",
width: "100%",
lineHeight: 1.5,
},
}}
/>
</main>
</ConnectKitProvider>
</siwe.Provider>
Expand Down
14 changes: 10 additions & 4 deletions src/pages/api/getUserNfts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@ 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
const allNftIds = await getAllUserNftIds(accountId);

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;
24 changes: 17 additions & 7 deletions src/pages/api/getUserUnlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,40 @@ 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
const allNftIds = await getAllUserNftIds(accountId);

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
Expand Down
10 changes: 9 additions & 1 deletion src/utils/loopring/getAllUserNftIds.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}`,
Expand Down

0 comments on commit 909374a

Please sign in to comment.