Skip to content

Commit

Permalink
Add PR353 the CORS and new useNativeCurrencyPrice
Browse files Browse the repository at this point in the history
  • Loading branch information
Nadai2010 committed Nov 19, 2024
1 parent 0891805 commit 0693218
Show file tree
Hide file tree
Showing 10 changed files with 1,687 additions and 1,300 deletions.
31 changes: 31 additions & 0 deletions packages/nextjs/app/api/price/[symbol]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export async function GET(
_: Request,
{ params: { symbol } }: { params: { symbol: string } },
) {
let apiUrl = "";
if (symbol === "ETH") {
apiUrl =
"https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd";
} else if (symbol === "STRK") {
apiUrl =
"https://api.coingecko.com/api/v3/simple/price?ids=starknet&vs_currencies=usd";
} else {
return Response.json({
ethereum: { usd: 0 },
starknet: { usd: 0 },
});
}
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`coingecko response status: ${response.status}`);
}
const json = await response.json();
return Response.json(json);
} catch (e) {
return Response.json({
ethereum: { usd: 0 },
starknet: { usd: 0 },
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { renderHook } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { useNativeCurrencyPrice } from "../useNativeCurrencyPrice";
import { priceService } from "~~/services/web3/PriceService";
import { useGlobalState } from "~~/services/store/store";

// Mock the store
vi.mock("~~/services/store/store", () => ({
useGlobalState: vi.fn(),
}));

// Mock the price service
vi.mock("~~/services/web3/PriceService", () => ({
priceService: {
getNextId: vi.fn(),
startPolling: vi.fn(),
stopPolling: vi.fn(),
},
}));

describe("useNativeCurrencyPrice", () => {
const mockSetNativeCurrencyPrice = vi.fn();
const mockSetStrkCurrencyPrice = vi.fn();
const mockIds = {
first: 123,
second: 124,
};

beforeEach(() => {
// Setup mocks
vi.mocked(useGlobalState).mockImplementation((selector) => {
if (selector.toString().includes("setNativeCurrencyPrice")) {
return mockSetNativeCurrencyPrice;
}
return mockSetStrkCurrencyPrice;
});

vi.mocked(priceService.getNextId).mockReturnValue(mockIds.first);
});

afterEach(() => {
vi.clearAllMocks();
});

it("should start polling on mount", () => {
renderHook(() => useNativeCurrencyPrice());

expect(priceService.getNextId).toHaveBeenCalled();
expect(priceService.startPolling).toHaveBeenCalledWith(
mockIds.first.toString(),
mockSetNativeCurrencyPrice,
mockSetStrkCurrencyPrice,
);
});

it("should stop polling on unmount", () => {
const { unmount } = renderHook(() => useNativeCurrencyPrice());

unmount();

expect(priceService.stopPolling).toHaveBeenCalledWith(
mockIds.first.toString(),
);
});

it("should maintain the same polling instance across rerenders", () => {
const { rerender } = renderHook(() => useNativeCurrencyPrice());
const firstCallArgs = vi.mocked(priceService.startPolling).mock.calls[0];

vi.clearAllMocks();

rerender();

expect(priceService.startPolling).not.toHaveBeenCalled();
expect(priceService.getNextId).toReturnWith(Number(firstCallArgs[0]));
});

it("should use store setters from global state", () => {
renderHook(() => useNativeCurrencyPrice());

expect(useGlobalState).toHaveBeenCalledWith(expect.any(Function));
expect(priceService.startPolling).toHaveBeenCalledWith(
mockIds.first.toString(),
mockSetNativeCurrencyPrice,
mockSetStrkCurrencyPrice,
);
});

it("should handle multiple instances correctly", () => {
vi.mocked(priceService.getNextId)
.mockReturnValueOnce(mockIds.first)
.mockReturnValueOnce(mockIds.second);

const { unmount: unmount1 } = renderHook(() => useNativeCurrencyPrice());
const { unmount: unmount2 } = renderHook(() => useNativeCurrencyPrice());

expect(priceService.startPolling).toHaveBeenNthCalledWith(
1,
mockIds.first.toString(),
mockSetNativeCurrencyPrice,
mockSetStrkCurrencyPrice,
);
expect(priceService.startPolling).toHaveBeenNthCalledWith(
2,
mockIds.second.toString(),
mockSetNativeCurrencyPrice,
mockSetStrkCurrencyPrice,
);

unmount1();
expect(priceService.stopPolling).toHaveBeenCalledWith(
mockIds.first.toString(),
);

unmount2();
expect(priceService.stopPolling).toHaveBeenCalledWith(
mockIds.second.toString(),
);
});

it("should handle errors in global state selectors gracefully", () => {
vi.mocked(useGlobalState).mockImplementation(() => {
return () => {};
});

expect(() => {
renderHook(() => useNativeCurrencyPrice());
}).not.toThrow();
});
});
45 changes: 10 additions & 35 deletions packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,21 @@
import { useEffect } from "react";
import { useTargetNetwork } from "./useTargetNetwork";
import { useInterval } from "usehooks-ts";
import scaffoldConfig from "~~/scaffold.config";
import { fetchPriceFromCoingecko } from "~~/utils/scaffold-stark";
import { useEffect, useRef } from "react";
import { useGlobalState } from "~~/services/store/store";
import { priceService } from "~~/services/web3/PriceService";

/**
* Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK
*/
export const useNativeCurrencyPrice = () => {
const { targetNetwork } = useTargetNetwork();
const nativeCurrencyPrice = useGlobalState(
(state) => state.nativeCurrencyPrice,
);
const strkCurrencyPrice = useGlobalState((state) => state.strkCurrencyPrice);
const setNativeCurrencyPrice = useGlobalState(
(state) => state.setNativeCurrencyPrice,
);
const setStrkCurrencyPrice = useGlobalState(
(state) => state.setStrkCurrencyPrice,
);
// Get the price of ETH & STRK from Coingecko on mount
const ref = useRef<string>(priceService.getNextId().toString());
useEffect(() => {
(async () => {
if (nativeCurrencyPrice == 0) {
const price = await fetchPriceFromCoingecko("ETH");
setNativeCurrencyPrice(price);
}
if (strkCurrencyPrice == 0) {
const strkPrice = await fetchPriceFromCoingecko("STRK");
setStrkCurrencyPrice(strkPrice);
}
})();
}, [targetNetwork]);

// Get the price of ETH & STRK from Coingecko at a given interval
useInterval(async () => {
const price = await fetchPriceFromCoingecko("ETH");
setNativeCurrencyPrice(price);
const strkPrice = await fetchPriceFromCoingecko("STRK");
setStrkCurrencyPrice(strkPrice);
}, scaffoldConfig.pollingInterval);

//return nativeCurrencyPrice;
const id = ref.current;
priceService.startPolling(id, setNativeCurrencyPrice, setStrkCurrencyPrice);
return () => {
priceService.stopPolling(id);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
4 changes: 2 additions & 2 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/themes": "2.0.3",
"@starknet-io/types-js": "^0.7.7",
"@starknet-react/chains": "^3.0.0",
"@starknet-react/core": "^3.0.0",
"@starknet-react/chains": "^3.1.0",
"@starknet-react/core": "^3.5.0",
"@starknet-react/typescript-config": "0.0.0",
"abi-wan-kanabi": "^2.2.2",
"blo": "^1.1.1",
Expand Down
116 changes: 116 additions & 0 deletions packages/nextjs/services/web3/PriceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import scaffoldConfig from "~~/scaffold.config";

export const fetchPriceFromCoingecko = async (
symbol: string,
retries = 3,
): Promise<number> => {
let attempt = 0;
while (attempt < retries) {
try {
const response = await fetch(`/api/price/${symbol}`);
const data = await response.json();
return symbol === "ETH" ? data.ethereum.usd : data.starknet.usd;
} catch (error) {
console.error(
`Attempt ${attempt + 1} - Error fetching ${symbol} price from Coingecko: `,
error,
);
attempt++;
if (attempt === retries) {
console.error(`Failed to fetch price after ${retries} attempts.`);
return 0;
}
}
}
return 0;
};

class PriceService {
private static instance: PriceService;
private intervalId: NodeJS.Timeout | null = null;
private listeners: Map<
any,
{
setNativeCurrencyPrice: (price: number) => void;
setStrkCurrencyPrice: (price: number) => void;
}
> = new Map();
private currentNativeCurrencyPrice: number = 0;
private currentStrkCurrencyPrice: number = 0;
private idCounter: number = 0;

private constructor() {}

static getInstance(): PriceService {
if (!PriceService.instance) {
PriceService.instance = new PriceService();
}
return PriceService.instance;
}

public getNextId(): number {
return ++this.idCounter;
}

public startPolling(
ref: any,
setNativeCurrencyPrice: (price: number) => void,
setStrkCurrencyPrice: (price: number) => void,
) {
if (this.listeners.has(ref)) return;
this.listeners.set(ref, { setNativeCurrencyPrice, setStrkCurrencyPrice });

if (this.intervalId) {
setNativeCurrencyPrice(this.currentNativeCurrencyPrice);
setStrkCurrencyPrice(this.currentStrkCurrencyPrice);
return;
}

this.fetchPrices();
this.intervalId = setInterval(() => {
this.fetchPrices();
}, scaffoldConfig.pollingInterval);
}

public stopPolling(ref: any) {
if (!this.intervalId) return;
if (!this.listeners.has(ref)) return;

this.listeners.delete(ref);
if (this.listeners.size === 0) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}

public getCurrentNativeCurrencyPrice() {
return this.currentNativeCurrencyPrice;
}

public getCurrentStrkCurrencyPrice() {
return this.currentStrkCurrencyPrice;
}

private async fetchPrices() {
try {
const ethPrice = await fetchPriceFromCoingecko("ETH");
const strkPrice = await fetchPriceFromCoingecko("STRK");
if (ethPrice && strkPrice) {
this.currentNativeCurrencyPrice = ethPrice;
this.currentStrkCurrencyPrice = strkPrice;
}
this.listeners.forEach((listener) => {
listener.setNativeCurrencyPrice(
ethPrice || this.currentNativeCurrencyPrice,
);
listener.setStrkCurrencyPrice(
strkPrice || this.currentStrkCurrencyPrice,
);
});
} catch (error) {
console.error("Error fetching prices:", error);
}
}
}

export const priceService = PriceService.getInstance();
Loading

0 comments on commit 0693218

Please sign in to comment.