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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
mintEndInstantUtcForMintDay,
mintStartInstantUtcForMintDay,
nextMintDateOnOrAfter,
wallTimeToUtcInstantInZone,
} from "@/components/meme-calendar/meme-calendar.helpers";

const formatAthensTime = (date: Date): string =>
new Intl.DateTimeFormat("en-GB", {
timeZone: "Europe/Athens",
hour12: false,
hour: "2-digit",
minute: "2-digit",
}).format(date);

const isoDate = (y: number, m: number, d: number): Date =>
new Date(Date.UTC(y, m, d));

describe("meme calendar timezone handling", () => {
it("keeps mint start anchored to 17:40 Athens time across 2024", () => {
const months = Array.from({ length: 12 }, (_, idx) => idx);

for (const month of months) {
const mintDay = nextMintDateOnOrAfter(isoDate(2024, month, 1));
const mintStart = mintStartInstantUtcForMintDay(mintDay);
const mintEnd = mintEndInstantUtcForMintDay(mintDay);

expect(formatAthensTime(mintStart)).toBe("17:40");
expect(formatAthensTime(mintEnd)).toBe("17:00");
expect(mintEnd.getTime()).toBeGreaterThan(mintStart.getTime());
}
});

it("applies the spring DST shift only after the EU transition completes", () => {
const beforeShiftMint = nextMintDateOnOrAfter(isoDate(2024, 2, 10)); // Mon, 11 Mar 2024 (still winter offset)
expect(beforeShiftMint.toISOString()).toBe("2024-03-11T00:00:00.000Z");

const beforeShiftStart = mintStartInstantUtcForMintDay(beforeShiftMint);
const beforeShiftEnd = mintEndInstantUtcForMintDay(beforeShiftMint);

expect(beforeShiftStart.toISOString()).toBe("2024-03-11T15:40:00.000Z");
expect(beforeShiftEnd.toISOString()).toBe("2024-03-12T15:00:00.000Z");
expect(formatAthensTime(beforeShiftStart)).toBe("17:40");
expect(formatAthensTime(beforeShiftEnd)).toBe("17:00");

const afterShiftMint = nextMintDateOnOrAfter(isoDate(2024, 3, 1)); // Mon, 1 Apr 2024
expect(afterShiftMint.toISOString()).toBe("2024-04-01T00:00:00.000Z");

const afterShiftStart = mintStartInstantUtcForMintDay(afterShiftMint);
const afterShiftEnd = mintEndInstantUtcForMintDay(afterShiftMint);

expect(afterShiftStart.toISOString()).toBe("2024-04-01T14:40:00.000Z");
expect(afterShiftEnd.toISOString()).toBe("2024-04-02T14:00:00.000Z");
expect(formatAthensTime(afterShiftStart)).toBe("17:40");
expect(formatAthensTime(afterShiftEnd)).toBe("17:00");
});

it("handles the autumn DST rollback without breaking mint anchors", () => {
const beforeRollbackMint = nextMintDateOnOrAfter(isoDate(2024, 9, 24)); // Fri, 25 Oct 2024
expect(beforeRollbackMint.toISOString()).toBe("2024-10-25T00:00:00.000Z");

const beforeRollbackStart = mintStartInstantUtcForMintDay(beforeRollbackMint);
const beforeRollbackEnd = mintEndInstantUtcForMintDay(beforeRollbackMint);

expect(beforeRollbackStart.toISOString()).toBe("2024-10-25T14:40:00.000Z");
expect(beforeRollbackEnd.toISOString()).toBe("2024-10-26T14:00:00.000Z");
expect(formatAthensTime(beforeRollbackStart)).toBe("17:40");
expect(formatAthensTime(beforeRollbackEnd)).toBe("17:00");

const afterRollbackMint = nextMintDateOnOrAfter(isoDate(2024, 9, 27)); // Mon, 28 Oct 2024
expect(afterRollbackMint.toISOString()).toBe("2024-10-28T00:00:00.000Z");

const afterRollbackStart = mintStartInstantUtcForMintDay(afterRollbackMint);
const afterRollbackEnd = mintEndInstantUtcForMintDay(afterRollbackMint);

expect(afterRollbackStart.toISOString()).toBe("2024-10-28T15:40:00.000Z");
expect(afterRollbackEnd.toISOString()).toBe("2024-10-29T15:00:00.000Z");
expect(formatAthensTime(afterRollbackStart)).toBe("17:40");
expect(formatAthensTime(afterRollbackEnd)).toBe("17:00");
});

it("shares phase wall-clock logic via wallTimeToUtcInstantInZone", () => {
const summerMintDay = isoDate(2024, 6, 1);
const winterMintDay = isoDate(2024, 0, 3);

const phaseWallTimes: Array<[number, number]> = [
[17, 40],
[18, 20],
[18, 30],
[18, 50],
[19, 0],
[19, 20],
];

const getPhaseUtcInstants = (mintDay: Date) =>
phaseWallTimes.map(([hour, minute]) =>
wallTimeToUtcInstantInZone(mintDay, hour, minute)
);

const summerPhaseTimes = getPhaseUtcInstants(summerMintDay);
const winterPhaseTimes = getPhaseUtcInstants(winterMintDay);

const expectedWallTimes = phaseWallTimes.map(
([hour, minute]) =>
`${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`
);

expect(summerPhaseTimes.map(formatAthensTime)).toEqual(expectedWallTimes);
expect(winterPhaseTimes.map(formatAthensTime)).toEqual(expectedWallTimes);

expect(summerPhaseTimes[0].toISOString()).toBe("2024-07-01T14:40:00.000Z");
expect(winterPhaseTimes[0].toISOString()).toBe("2024-01-03T15:40:00.000Z");
});
});
2 changes: 1 addition & 1 deletion components/meme-calendar/meme-calendar.helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const SZN1_RANGE = {
} as const;
export const SZN1_SEASON_INDEX = -13;

const EUROPE_TZ = "Europe/Nicosia"; // EET/EEST
const EUROPE_TZ = "Europe/Athens"; // EET/EEST
const EUROPE_WINTER_OFFSET_HOURS = +2; // EET (UTC+2)
const EUROPE_SUMMER_OFFSET_HOURS = +3; // EEST (UTC+3)
const MINT_EUROPE_HOUR = 17;
Expand Down
119 changes: 73 additions & 46 deletions hooks/useManifoldClaim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ export enum ManifoldPhase {
PUBLIC = "Public Phase",
}

interface PhaseBoundary {
hour: number;
minute: number;
dayOffset?: number;
}

interface PhaseDefinition {
id: string;
name: string;
type: ManifoldPhase;
start: PhaseBoundary;
end: PhaseBoundary;
}

export interface MemePhase {
id: string;
name: string;
Expand All @@ -33,52 +47,59 @@ export interface MemePhase {
end: Time;
}

export function buildMemesPhases(mintDate: Time): MemePhase[] {
const buildTime = (
hour: number,
minute: number,
nextDay: boolean = false
) => {
const ref = nextDay ? mintDate.plusDays(1) : mintDate;
return Time.fromString(
wallTimeToUtcInstantInZone(ref.toDate(), hour, minute).toISOString()
const PHASE_DEFINITIONS: PhaseDefinition[] = [
{
id: "0",
name: "Phase 0 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: { hour: 17, minute: 40 },
end: { hour: 18, minute: 20 },
},
{
id: "1",
name: "Phase 1 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: { hour: 18, minute: 30 },
end: { hour: 18, minute: 50 },
},
{
id: "2",
name: "Phase 2 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: { hour: 19, minute: 0 },
end: { hour: 19, minute: 20 },
},
{
id: "public",
name: "Public Phase",
type: ManifoldPhase.PUBLIC,
start: { hour: 19, minute: 20 },
end: { hour: 17, minute: 0, dayOffset: 1 },
},
];

export function buildMemesPhases(mintDate: Time = Time.now()): MemePhase[] {
const resolveTime = ({
hour,
minute,
dayOffset = 0,
}: PhaseBoundary): Time => {
const reference = dayOffset === 0 ? mintDate : mintDate.plusDays(dayOffset);
const instant = wallTimeToUtcInstantInZone(
reference.toDate(),
hour,
minute
);
return Time.millis(instant.getTime());
};

return [
{
id: "0",
name: "Phase 0 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: buildTime(17, 40),
end: buildTime(18, 20),
},
{
id: "1",
name: "Phase 1 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: buildTime(18, 30),
end: buildTime(18, 50),
},
{
id: "2",
name: "Phase 2 (Allowlist)",
type: ManifoldPhase.ALLOWLIST,
start: buildTime(19, 0),
end: buildTime(19, 20),
},
{
id: "public",
name: "Public Phase",
type: ManifoldPhase.PUBLIC,
start: buildTime(19, 20),
end: buildTime(17, 0, true),
},
];
return PHASE_DEFINITIONS.map(({ start, end, ...phase }) => ({
...phase,
start: resolveTime(start),
end: resolveTime(end),
}));
}

const MEME_PHASES = buildMemesPhases(Time.now());

export interface ManifoldClaim {
instanceId: number;
total: number;
Expand Down Expand Up @@ -106,7 +127,7 @@ export function useManifoldClaim(
const [refetchInterval, setRefetchInterval] = useState<number>(5000);

const getStatus = useCallback((start: number, end: number) => {
const now = Date.now() / 1000;
const now = Time.now().toSeconds();
if (now < start) {
return ManifoldClaimStatus.UPCOMING;
} else if (now >= start && now < end) {
Expand All @@ -116,17 +137,19 @@ export function useManifoldClaim(
}, []);

const getMemePhase = useCallback(
(phase: ManifoldPhase, end: number) => {
(phase: ManifoldPhase, start: number, end: number) => {
if (!areEqualAddresses(contract, MEMES_CONTRACT)) {
return undefined;
}

const memePhases = buildMemesPhases(Time.seconds(start));

if (phase === ManifoldPhase.PUBLIC) {
return MEME_PHASES.find((mp) => mp.id === "public");
return memePhases.find((mp) => mp.id === "public");
}

const endTime = Time.seconds(end);
return MEME_PHASES.find((mp) => mp.end.gte(endTime));
return memePhases.find((mp) => mp.end.gte(endTime));
},
[contract]
);
Expand Down Expand Up @@ -155,7 +178,11 @@ export function useManifoldClaim(
publicMerkle && claimData.total > 0
? ManifoldPhase.PUBLIC
: ManifoldPhase.ALLOWLIST;
const memePhase = getMemePhase(phase, claimData.endDate);
const memePhase = getMemePhase(
phase,
claimData.startDate,
claimData.endDate
);
const remaining = Number(claimData.totalMax) - Number(claimData.total);
const newClaim: ManifoldClaim = {
instanceId: instanceId,
Expand Down
22 changes: 4 additions & 18 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
Expand All @@ -18,18 +14,14 @@
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"@testing-library/jest-dom"
],
"types": ["@testing-library/jest-dom"],
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
},
"include": [
Expand All @@ -39,11 +31,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules",
"tests",
"e2e",
"**/__tests__/**",
"**/__mocks__/**"
]
"exclude": ["node_modules", "tests", "e2e", "**/__mocks__/**"]
}