@shogun-sdk/one-shot
Version:
Shogun SDK - One Shot: React Components and hooks for cross-chain swaps
195 lines (167 loc) • 5.18 kB
text/typescript
import { LEGO_API } from '@shogun-sdk/money-legos';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useShogunLego } from './../contexts/ShogunLegoContext.js';
/* ==============================
🔹 Type Definitions & Interfaces
============================== */
/** Response structure returned from the API */
interface ResponseData {
steps: Step[];
details: Details;
fees: Fees;
}
/** Transaction details including sender, recipient, and token info */
interface Details {
sender: string;
recipient: string;
tokenIn: TokenIn;
}
/** Represents a token, including metadata and amount */
interface TokenIn {
amountUsd: string;
chainId: number;
address: string;
symbol: string;
name: string;
decimals: number;
amount?: string;
}
/** Fee structure for transactions */
interface Fees {
gas: TokenIn;
}
/** Step object representing a blockchain transaction */
interface Step {
from: string;
to: string;
data: string;
value: string;
chainId: number;
gas: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
}
/** Represents an NFT item with contract address and token ID */
interface NFTItem {
address: string;
tokenId: string;
}
/** Defines the token being used for the transaction */
interface Token {
address: string;
decimals: number;
chainId: number;
}
/** Input parameters required for `useLego` */
interface UseLegoProps {
items: NFTItem[];
token: Token;
userAddress: string;
}
/** Output structure of `useLego` */
interface UseLegoResult {
status: boolean;
error?: string | null;
data?: ResponseData | null;
isLoading: boolean;
refetch: () => void;
}
/* ==============================
🔹 useLego Hook
============================== */
/**
* Hook to validate and prepare transaction data for purchasing NFTs.
*
* @param {UseLegoProps} props - The NFT items, token details, and user address.
* @returns {UseLegoResult} - Status, error message (if any), response data, loading state, and a refetch function.
*/
export const useLego = ({ items, token, userAddress }: UseLegoProps): UseLegoResult => {
const { API_KEY } = useShogunLego();
// ✅ State for handling API responses, errors, and loading status
const [data, setData] = useState<ResponseData | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
// ✅ Ref to manage API request cancellation
const abortControllerRef = useRef<AbortController | null>(null);
// ✅ Memoized API request body to prevent unnecessary recalculations
const requestBody = useMemo(() => {
if (!items.length || !token.address || !userAddress) return null;
return JSON.stringify({
tokenIn: {
address: token.address,
decimals: token.decimals,
chainId: token.chainId,
},
nft: items,
address: userAddress,
});
}, [items, token.address, token.decimals, token.chainId, userAddress]);
// ✅ Function to fetch data from the API
const fetchData = useCallback(async () => {
if (!requestBody) {
setError('Invalid parameters provided to useLego hook.');
return;
}
// Cancel any ongoing request before making a new one
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create a new AbortController for the current request
const abortController = new AbortController();
abortControllerRef.current = abortController;
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${LEGO_API}/nft/purchase`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY,
},
body: requestBody,
signal: abortController.signal, // ✅ Allows request cancellation
});
// Prevent state updates if request was aborted
if (abortController.signal.aborted) {
return;
}
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
const result = await response.json();
setData(result?.data?.execute);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
console.error('NFT Purchase API Error:', err);
setError(err.message);
}
} finally {
// Ensure loading state resets only if this is the latest request
if (abortControllerRef.current === abortController) {
setIsLoading(false);
abortControllerRef.current = null;
}
}
}, [API_KEY, requestBody]);
// ✅ Auto-fetch data when dependencies change
useEffect(() => {
if (requestBody) {
fetchData();
}
// Cleanup function to abort any ongoing requests when dependencies change
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
};
}, [fetchData, requestBody]);
// ✅ Return hook state and refetch function
return {
status: !error && !!data,
error,
data,
isLoading,
refetch: fetchData,
};
};