forked from Scaffold-Stark/scaffold-stark-2
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add PR353 the CORS and new useNativeCurrencyPrice
- Loading branch information
Showing
10 changed files
with
1,687 additions
and
1,300 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | ||
}); | ||
} | ||
} |
130 changes: 130 additions & 0 deletions
130
packages/nextjs/hooks/scaffold-stark/__tests__/useNativeCurrencyPrice.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
45
packages/nextjs/hooks/scaffold-stark/useNativeCurrencyPrice.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, []); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Oops, something went wrong.