Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,8 @@ NEXT_PUBLIC_AWS_RUM_IDENTITY_POOL_ID=
PEPE_CACHE_TTL_MINUTES=10
PEPE_CACHE_MAX_ITEMS=500
IPFS_GATEWAY=https://ipfs.io/ipfs/

# SERVER-SIDE CLIENT IDENTIFICATION (for SSR requests)
# Used to sign server-side API requests with HMAC-SHA256
SSR_CLIENT_ID=
SSR_CLIENT_SECRET=
14 changes: 10 additions & 4 deletions .github/workflows/build-upload-deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,18 @@ jobs:
--version-label "${{ env.COMMIT_SHA }}" \
--description "${{ env.COMMIT_MESSAGE }}"

- name: Deploy new Version to ElasticBeanstalk
- name: Deploy new Version to ElasticBeanstalk with Runtime Environment Variables
env:
SSR_CLIENT_ID: ${{ secrets.SSR_CLIENT_ID }}
SSR_CLIENT_SECRET: ${{ secrets.SSR_CLIENT_SECRET }}
run: |
aws elasticbeanstalk update-environment \
--application-name "${{ env.BEANSTALK_APP_NAME }}" \
--environment-name "${{ env.BEANSTALK_ENV_NAME }}" \
--version-label "${{ env.COMMIT_SHA }}"
--application-name "${{ env.BEANSTALK_APP_NAME }}" \
--environment-name "${{ env.BEANSTALK_ENV_NAME }}" \
--version-label "${{ env.COMMIT_SHA }}" \
--option-settings \
"Namespace=aws:elasticbeanstalk:application:environment,OptionName=SSR_CLIENT_ID,Value=${SSR_CLIENT_ID}" \
"Namespace=aws:elasticbeanstalk:application:environment,OptionName=SSR_CLIENT_SECRET,Value=${SSR_CLIENT_SECRET}"

- name: Check Elastic Beanstalk health and readiness (120s warmup then 20 retries with 60s delay)
run: |
Expand Down
5 changes: 2 additions & 3 deletions __tests__/app/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ jest.mock("@/components/latest-activity/LatestActivity", () =>


// Mock API fetch to prevent network calls
jest.mock("@/services/6529api", () => ({
fetchUrl: jest.fn().mockResolvedValue({ data: [] }),
fetchAllPages: jest.fn().mockResolvedValue([]),
jest.mock("@/services/api/common-api", () => ({
commonApiFetch: jest.fn().mockResolvedValue({ data: [], count: 0 }),
}));


Expand Down
14 changes: 12 additions & 2 deletions __tests__/services/common-api.more.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ describe("commonApi utility methods", () => {
it("commonApiPut posts JSON body", async () => {
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ ok: 1 }),
headers: new Headers({ "content-type": "application/json" }),
});
const res = await commonApiPut({ endpoint: "e", body: { a: 1 } });
expect(res).toEqual({ ok: 1 });
Expand All @@ -39,9 +41,11 @@ describe("commonApi utility methods", () => {
});

it("commonApiDeleteWithBody deletes with body", async () => {
(global.fetch as jest.Mock).mockResolvedValue({
(globalThis.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ r: 2 }),
headers: new Headers({ "content-type": "application/json" }),
});
const res = await commonApiDeleteWithBody({
endpoint: "del",
Expand All @@ -63,7 +67,11 @@ describe("commonApi utility methods", () => {
});

it("commonApiDelete sends DELETE request", async () => {
(global.fetch as jest.Mock).mockResolvedValue({ ok: true });
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
headers: new Headers(),
});
await commonApiDelete({ endpoint: "x" });
expect(globalThis.fetch).toHaveBeenCalledWith(
"https://api.test.6529.io/api/x",
Expand All @@ -82,7 +90,9 @@ describe("commonApi utility methods", () => {
const form = new FormData();
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ res: 3 }),
headers: new Headers({ "content-type": "application/json" }),
});
const { commonApiPostForm } = await import("@/services/api/common-api");
const result = await commonApiPostForm({ endpoint: "f", body: form });
Expand Down
3 changes: 2 additions & 1 deletion __tests__/services/common-api.postNoBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ describe("commonApiPostWithoutBodyAndResponse", () => {
it("rejects with error body", async () => {
(global.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 400,
statusText: "x",
json: async () => ({ error: "err" }),
});
await expect(
commonApiPostWithoutBodyAndResponse({ endpoint: "e" })
).rejects.toBe("err");
).rejects.toThrow("HTTP 400 x: err");
});
});
18 changes: 11 additions & 7 deletions __tests__/services/common-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("commonApiFetch", () => {
Authorization: "Bearer jwt",
}),
signal: undefined,
}),
})
);
expect(result).toEqual({ result: 1 });
});
Expand All @@ -45,17 +45,20 @@ describe("commonApiFetch", () => {
(getAuthJwt as jest.Mock).mockReturnValue(null);
fetchMock.mockResolvedValue({
ok: false,
status: 400,
statusText: "Bad",
json: async () => ({ error: "err" }),
});

await expect(commonApiFetch({ endpoint: "bad" })).rejects.toBe("err");
await expect(commonApiFetch({ endpoint: "bad" })).rejects.toThrow(
"HTTP 400 Bad: err"
);
expect(fetchMock).toHaveBeenCalledWith(
"https://api.test.6529.io/api/bad",
expect.objectContaining({
headers: {},
signal: undefined,
}),
})
);
});
});
Expand Down Expand Up @@ -87,7 +90,7 @@ describe("commonApiPost", () => {
"x-6529-auth": "a",
}),
body: JSON.stringify({ v: 1 }),
}),
})
);
expect(result).toEqual({ res: 1 });
});
Expand All @@ -97,12 +100,13 @@ describe("commonApiPost", () => {
(getAuthJwt as jest.Mock).mockReturnValue(null);
fetchMock.mockResolvedValue({
ok: false,
status: 400,
statusText: "B",
json: async () => ({ error: "err" }),
});

await expect(commonApiPost({ endpoint: "e", body: {} })).rejects.toBe(
"err",
await expect(commonApiPost({ endpoint: "e", body: {} })).rejects.toThrow(
"HTTP 400 B: err"
);
expect(fetchMock).toHaveBeenCalledWith(
"https://api.test.6529.io/api/e",
Expand All @@ -112,7 +116,7 @@ describe("commonApiPost", () => {
"Content-Type": "application/json",
}),
body: JSON.stringify({}),
}),
})
);
});
});
3 changes: 3 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export const fetchCache = "force-no-store";

// Side effect: Overrides globalThis.fetch on server-side to automatically
// add auth headers (x-6529-internal-*) for rate limiter/WAF bypass
import "@/lib/fetch/ssrFetch";
import "@/components/drops/create/lexical/lexical.styles.scss";
import "@/styles/Home.module.scss";
import "@/styles/seize-bootstrap.scss";
Expand Down
4 changes: 2 additions & 2 deletions components/latest-activity/LatestActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use client";

import { useEffect, useState } from "react";
import { Container, Row } from "react-bootstrap";
import { NFT } from "@/entities/INFT";
import { NextGenCollection } from "@/entities/INextgen";
import { Transaction } from "@/entities/ITransaction";
import useIsMobileScreen from "@/hooks/isMobileScreen";
import { useActivityData } from "@/hooks/useActivityData";
import { useActivityFilters } from "@/hooks/useActivityFilters";
import { useNFTCollections } from "@/hooks/useNFTCollections";
import { useEffect, useState } from "react";
import { Container, Row } from "react-bootstrap";
import Pagination from "../pagination/Pagination";
import ActivityFilters from "./ActivityFilters";
import ActivityHeader from "./ActivityHeader";
Expand Down
59 changes: 31 additions & 28 deletions components/latest-activity/fetchInitialActivityData.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { publicEnv } from "@/config/env";
import { DBResponse } from "@/entities/IDBResponse";
import { NFT } from "@/entities/INFT";
import { NextGenCollection } from "@/entities/INextgen";
import { Transaction } from "@/entities/ITransaction";
import { fetchAllPages, fetchUrl } from "@/services/6529api";
import { commonApiFetch } from "@/services/api/common-api";

export interface InitialActivityData {
Expand All @@ -18,38 +16,43 @@ export async function fetchInitialActivityData(
pageSize: number = 50
): Promise<InitialActivityData> {
try {
// Build activity API URL with default filters (All/All)
const activityUrl = `${publicEnv.API_ENDPOINT}/api/transactions?page_size=${pageSize}&page=${page}`;

// Fetch all data in parallel
const [activityResponse, memesResponse, gradientsData, nextgenResponse] =
await Promise.all([
// Activity data
fetchUrl(activityUrl) as Promise<DBResponse>,
const [
activityResponse,
memesResponse,
gradientsResponse,
nextgenResponse,
] = await Promise.all([
// Activity data
commonApiFetch<DBResponse>({
endpoint: "transactions",
params: {
page_size: String(pageSize),
page: String(page),
},
}),

// Memes data
fetchUrl(
`${publicEnv.API_ENDPOINT}/api/memes_lite`
) as Promise<DBResponse>,
// Memes data
commonApiFetch<DBResponse<NFT>>({
endpoint: "memes_lite",
}),

// Gradients data
fetchAllPages<NFT>(
`${publicEnv.API_ENDPOINT}/api/nfts/gradients?&page_size=101`
),
// Gradients data (first page only, page_size 101)
commonApiFetch<DBResponse<NFT>>({
endpoint: "nfts/gradients",
params: {
page_size: "101",
},
}),

// NextGen collections
commonApiFetch<{
count: number;
page: number;
next: any;
data: NextGenCollection[];
}>({
endpoint: `nextgen/collections`,
}),
]);
// NextGen collections
commonApiFetch<DBResponse<NextGenCollection>>({
endpoint: `nextgen/collections`,
}),
]);

// Combine memes and gradients
const nfts = [...memesResponse.data, ...gradientsData];
const nfts = [...memesResponse.data, ...gradientsResponse.data];

return {
activity: activityResponse.data,
Expand Down
12 changes: 5 additions & 7 deletions components/layout/SmallScreenHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";

import React from "react";
import Link from "next/link";
import Image from "next/image";
import { Bars3Icon } from "@heroicons/react/24/outline";
import HeaderSearchButton from "@/components/header/header-search/HeaderSearchButton";
import { Bars3Icon } from "@heroicons/react/24/outline";
import Image from "next/image";
import Link from "next/link";

interface SmallScreenHeaderProps {
readonly onMenuToggle: () => void;
Expand All @@ -24,7 +23,7 @@ export default function SmallScreenHeader({
loading="eager"
priority
alt="6529Seize"
src="/6529.png"
src="/6529.svg"
className="tw-h-10 tw-w-10 tw-flex-shrink-0 tw-transition-all tw-duration-100 desktop-hover:hover:tw-scale-[1.02] desktop-hover:hover:tw-shadow-[0_0_20px_10px_rgba(255,215,215,0.3)]"
width={40}
height={40}
Expand All @@ -35,8 +34,7 @@ export default function SmallScreenHeader({
<button
onClick={onMenuToggle}
className="tw-flex tw-items-center tw-justify-center tw-rounded-lg tw-h-10 tw-w-10 tw-border-0 tw-text-iron-300 desktop-hover:hover:tw-text-iron-50 tw-shadow-sm focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-primary-400 tw-transition tw-duration-300 tw-ease-out tw-bg-iron-800 tw-ring-1 tw-ring-inset tw-ring-iron-700 desktop-hover:hover:tw-bg-iron-700"
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
>
aria-label={isMenuOpen ? "Close menu" : "Open menu"}>
<Bars3Icon className="tw-h-5 tw-w-5 tw-text-iron-300 tw-flex-shrink-0" />
</button>
</div>
Expand Down
Loading
Loading