diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts new file mode 100644 index 0000000..09b485f --- /dev/null +++ b/src/react/hooks/index.ts @@ -0,0 +1,7 @@ +export { useCancelOrder } from './useCancelOrder'; +export { useCreateBasedOrder } from './useCreateBasedOrder'; +export { useCreateOrder } from './useCreateOrder'; +export { useFulfillOrder } from './useFulfillOrder'; +export { useOrders } from './useOrders'; +export { useQuickSwap } from './useQuickSwap'; +export { useWallet } from './useWallet'; diff --git a/src/react/hooks/useCancelOrder.ts b/src/react/hooks/useCancelOrder.ts new file mode 100644 index 0000000..6284b77 --- /dev/null +++ b/src/react/hooks/useCancelOrder.ts @@ -0,0 +1,35 @@ +import { BigNumberish, ContractReceipt } from 'ethers'; +import { useContext } from 'react'; + +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +/** + * Get a function to cancel an order by nonce and type + */ +export function useCancelOrder() { + const { nftSwap } = useContext(SwapSdkContext); + + /** + * Cancel an order by nonce and type + * @param nonce nonce of the order to be cancel + * @param orderType type of token from the order (ERC721 or ERC1155) + * @returns a transaction receipt if successful + */ + const cancelOrder = async ( + nonce: BigNumberish, + orderType: 'ERC721' | 'ERC1155' + ): Promise => { + if (!nftSwap) return; + + try { + const cancelTx = await nftSwap.cancelOrder(nonce, orderType); + const cancelTxReceipt = await cancelTx.wait(); + return cancelTxReceipt; + } catch (error) { + console.error(error); + return undefined; + } + }; + + return cancelOrder; +} diff --git a/src/react/hooks/useCreateBasedOrder.ts b/src/react/hooks/useCreateBasedOrder.ts new file mode 100644 index 0000000..1194d77 --- /dev/null +++ b/src/react/hooks/useCreateBasedOrder.ts @@ -0,0 +1,56 @@ +import { useContext } from 'react'; + +import { + SignedNftOrderV4, + UserFacingERC20AssetDataSerializedV4, +} from '../../sdk'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +export interface NftCollection { + tokenAddress: string; + type: 'ERC721' | 'ERC1155'; +} + +/** + * Get the function to create order from a specific collection + */ +export function useCreateBasedOrder() { + const { nftSwap } = useContext(SwapSdkContext); + + /** + * Create an order with any NFT from specific collection maker has and post it in the orderbook if successful + * @param erc20Asset an object that contains the type "ERC20", the amount to sell and the address of the token + * @param nftCollectionAsset an object that contains the type of NFT to sell (ERC721 or ERC1155) and NFT collection address + * @param makerAddress wallet address of order creator + * @param chainId id of the chain in which the transaction will be performed + * @param metadata an optional record object that will be stored with the order in the orderbook + * @returns signed order + */ + const createBasedOrder = async ( + erc20Asset: UserFacingERC20AssetDataSerializedV4, + nftCollectionAsset: NftCollection, + makerAddress: string | undefined, + chainId: number | string, + metadata?: Record + ): Promise => { + if (!nftSwap) return; + if (!makerAddress) return; + + try { + const collectionBasedOrder = nftSwap.buildCollectionBasedOrder( + erc20Asset, + nftCollectionAsset, + makerAddress + ); + + const signedOrder = await nftSwap.signOrder(collectionBasedOrder); + await nftSwap.postOrder(signedOrder, chainId, metadata); + return signedOrder; + } catch (error) { + console.error(error); + return undefined; + } + }; + + return createBasedOrder; +} diff --git a/src/react/hooks/useCreateOrder.ts b/src/react/hooks/useCreateOrder.ts new file mode 100644 index 0000000..d3a4070 --- /dev/null +++ b/src/react/hooks/useCreateOrder.ts @@ -0,0 +1,99 @@ +import { useContext } from 'react'; + +import { Fee, SignedNftOrderV4, SwappableAssetV4 } from '../../sdk'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +/** + * Get the order creation function + */ +export function useCreateOrder() { + const { nftSwap } = useContext(SwapSdkContext); + + /** + * Create an order and post it in the orderbook if successful + * @param makerAsset an asset (ERC20, ERC721, or ERC1155) the user has + * @param takerAsset an asset (ERC20, ERC721, or ERC1155) the user wants + * @param makerAddress wallet address of user who creates the order + * @param chainId id of the chain in which the transaction will be performed + * @param metadata an optional record object that will be stored with the order in the orderbook + * @param fees optional array that contents config for fee and royalties + * @returns signed order + */ + const createOrder = async ( + makerAsset: SwappableAssetV4, + takerAsset: SwappableAssetV4, + makerAddress: string | undefined, + chainId: number | string, + metadata?: Record, + fees?: Fee[] + ): Promise => { + if (!nftSwap) return; + if (!makerAddress) return; + + const approvalStatus = await nftSwap.loadApprovalStatus( + makerAsset, + makerAddress + ); + if (!approvalStatus.contractApproved) { + const approvalTx = await nftSwap.approveTokenOrNftByAsset( + makerAsset, + makerAddress + ); + await approvalTx.wait(); + } + + let signedOrder: SignedNftOrderV4 | null = null; + + if (makerAsset.type === 'ERC20' && takerAsset.type === 'ERC721') { + const ercToNftOrder = nftSwap.buildOrder( + makerAsset, + takerAsset, + makerAddress, + { + fees, + } + ); + signedOrder = await nftSwap.signOrder(ercToNftOrder); + } + if (makerAsset.type === 'ERC721' && takerAsset.type === 'ERC20') { + const nftToErcOrder = nftSwap.buildOrder( + makerAsset, + takerAsset, + makerAddress, + { + fees, + } + ); + signedOrder = await nftSwap.signOrder(nftToErcOrder); + } + if (makerAsset.type === 'ERC20' && takerAsset.type === 'ERC1155') { + const ercToMtOrder = nftSwap.buildOrder( + makerAsset, + takerAsset, + makerAddress, + { + fees, + } + ); + signedOrder = await nftSwap.signOrder(ercToMtOrder); + } + if (makerAsset.type === 'ERC1155' && takerAsset.type === 'ERC20') { + const mtToErcOrder = nftSwap.buildOrder( + makerAsset, + takerAsset, + makerAddress, + { + fees, + } + ); + signedOrder = await nftSwap.signOrder(mtToErcOrder); + } + + if (!signedOrder) return; + + await nftSwap.postOrder(signedOrder, chainId, metadata); + return signedOrder; + }; + + return createOrder; +} diff --git a/src/react/hooks/useFulfillOrder.ts b/src/react/hooks/useFulfillOrder.ts new file mode 100644 index 0000000..c06582c --- /dev/null +++ b/src/react/hooks/useFulfillOrder.ts @@ -0,0 +1,99 @@ +import { ContractReceipt } from 'ethers'; + +import { useContext } from 'react'; + +import { + ApprovalOverrides, + FillOrderOverrides, + SwappableAssetV4, +} from '../../sdk'; +import { PayableOverrides, TransactionOverrides } from '../../sdk/common/types'; +import { PostOrderResponsePayload } from '../../sdk/v4/orderbook'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +/** + * Get the order fulfillment function + */ +export function useFulfillOrder() { + const { nftSwap } = useContext(SwapSdkContext); + + /** + * Fulfills signed order + * @param order order to fulfill + * @param takerAddress buyer wallet address + * @param approvalOverrides optional config for approval status load + * @param approvalTransactionOverrides optional config for transaction approve + * @param fillOrderOverrides optional config for order fulfillment + * @param transactionOverrides optional config for swap transaction + * @returns a transaction receipt if successful + */ + const fulfillOrder = async ( + order: PostOrderResponsePayload | undefined, + takerAddress: string | undefined, + approvalOverrides?: Partial, + approvalTransactionOverrides?: Partial, + fillOrderOverrides?: Partial, + transactionOverrides?: Partial + ): Promise => { + if (!nftSwap) return; + if (!order) return; + if (!takerAddress) return; + + let takerAsset: SwappableAssetV4 | null = null; + + switch (order.nftType) { + case 'ERC20': + takerAsset = { + tokenAddress: order.erc20Token, + amount: order.erc20TokenAmount, + type: 'ERC20', + }; + break; + case 'ERC721': + takerAsset = { + tokenAddress: order.nftToken, + tokenId: order.nftTokenId, + type: 'ERC721', + }; + break; + case 'ERC1155': + takerAsset = { + tokenAddress: order.nftToken, + tokenId: order.nftTokenId, + amount: order.erc20TokenAmount, + type: 'ERC1155', + }; + break; + default: + takerAsset = null; + break; + } + + if (!takerAsset) return; + + const approvalStatus = await nftSwap.loadApprovalStatus( + takerAsset, + takerAddress, + approvalOverrides + ); + if (!approvalStatus.contractApproved) { + const approvalTx = await nftSwap.approveTokenOrNftByAsset( + takerAsset, + takerAddress, + approvalTransactionOverrides + ); + await approvalTx.wait(); + } + + const fillTx = await nftSwap.fillSignedOrder( + order.order, + fillOrderOverrides, + transactionOverrides + ); + const fillTxReceipt = await fillTx.wait(); + + return fillTxReceipt; + }; + + return fulfillOrder; +} diff --git a/src/react/hooks/useOrders.ts b/src/react/hooks/useOrders.ts new file mode 100644 index 0000000..41175c7 --- /dev/null +++ b/src/react/hooks/useOrders.ts @@ -0,0 +1,41 @@ +import { useContext, useEffect, useState } from 'react'; + +import { + PostOrderResponsePayload, + SearchOrdersParams, +} from '../../sdk/v4/orderbook'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +/** + * Get orders from the Trader.xyz Open NFT Orderbook + * @param searchParams optional conditions to search for specific orders + * @returns an array of orders and a function to refresh it + */ +export function useOrders(searchParams?: Partial) { + const { signer, nftSwap } = useContext(SwapSdkContext); + + const [orders, setOrders] = useState(); + + const fetchOrders = async (): Promise => { + if (!signer) return; + if (!nftSwap) return; + + try { + const fetchedOrdersData = await nftSwap.getOrders(searchParams); + const fetchedOrders = fetchedOrdersData.orders; + setOrders(fetchedOrders); + } catch (error: any) { + console.error(`Unable to load orders:\n${error.message}`); + setOrders(undefined); + } + }; + + useEffect(() => { + fetchOrders(); + }, [signer, nftSwap]); + + return [orders, fetchOrders] as [ + PostOrderResponsePayload[] | undefined, + () => Promise + ]; +} diff --git a/src/react/hooks/useQuickSwap.ts b/src/react/hooks/useQuickSwap.ts new file mode 100644 index 0000000..78df6fa --- /dev/null +++ b/src/react/hooks/useQuickSwap.ts @@ -0,0 +1,107 @@ +import { ContractReceipt } from 'ethers'; +import { useContext } from 'react'; + +import { Fee, SignedNftOrderV4Serialized, SwappableAssetV4 } from '../../sdk'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; +import { useCancelOrder } from './useCancelOrder'; +import { useCreateOrder } from './useCreateOrder'; + +/** + * Get the function to create an order and instantly execute transaction with the matching order of opposite direction + */ +export function useQuickSwap() { + const { nftSwap } = useContext(SwapSdkContext); + + const createOrder = useCreateOrder(); + const cancelOrder = useCancelOrder(); + + /** + * Create a new order, look up an existing matching order, and execute the transaction (do not work with native currency e.g. ETH) + * @param makerAsset an asset (ERC20, ERC721, or ERC1155) the user has + * @param takerAsset an asset (ERC20, ERC721, or ERC1155) the user wants + * @param makerAddress wallet address of user who creates the order + * @param chainId id of the chain in which the transaction will be performed + * @param metadata an optional record object that will be stored with the order in the orderbook + * @param fees optional array that contents config for fee and royalties + * @returns a swap transaction receipt if successful and cancel transaction receipt if there are no matching orders + */ + const quickSwap = async ( + makerAsset: SwappableAssetV4, + takerAsset: SwappableAssetV4, + makerAddress: string | undefined, + chainId: number | string, + metadata?: Record, + fees?: Fee[] + ): Promise => { + if (!nftSwap) return; + + const newOrder = await createOrder( + makerAsset, + takerAsset, + makerAddress, + chainId, + metadata, + fees + ); + if (!newOrder) return; + + try { + let matchingOrder: SignedNftOrderV4Serialized | null = null; + + if (makerAsset.type !== 'ERC20') { + const ordersData = await nftSwap.getOrders({ + nftToken: makerAsset.tokenAddress, + nftTokenId: makerAsset.tokenId, + nftType: makerAsset.type, + }); + + const orderToBuy = ordersData.orders.find( + (order) => order.sellOrBuyNft === 'buy' + ); + if (!orderToBuy) { + const cancelTxReceipt = await cancelOrder( + newOrder.nonce, + makerAsset.type + ); + return cancelTxReceipt; + } + + matchingOrder = orderToBuy.order; + const matchTx = await nftSwap.matchOrders(newOrder, matchingOrder); + const matchTxReceipt = await matchTx.wait(); + return matchTxReceipt; + } + + if (makerAsset.type === 'ERC20' && takerAsset.type !== 'ERC20') { + const ordersData = await nftSwap.getOrders({ + nftToken: takerAsset.tokenAddress, + nftTokenId: takerAsset.tokenId, + nftType: takerAsset.type, + }); + + const orderToBuy = ordersData.orders.find( + (order) => order.sellOrBuyNft === 'sell' + ); + if (!orderToBuy) { + const cancelTxReceipt = await cancelOrder( + newOrder.nonce, + takerAsset.type + ); + return cancelTxReceipt; + } + + matchingOrder = orderToBuy.order; + const matchTx = await nftSwap.matchOrders(matchingOrder, newOrder); + const matchTxReceipt = await matchTx.wait(); + return matchTxReceipt; + } + + return undefined; + } catch (error) { + console.error(error); + return undefined; + } + }; + + return quickSwap; +} diff --git a/src/react/hooks/useWallet.ts b/src/react/hooks/useWallet.ts new file mode 100644 index 0000000..9e0ca87 --- /dev/null +++ b/src/react/hooks/useWallet.ts @@ -0,0 +1,26 @@ +import { useContext } from 'react'; +import { SwapSdkContext } from '../providers/swapSdkProvider'; + +export const useWallet = () => { + const { + provider, + signer, + network, + chainId, + account, + balance, + connectWallet, + disconnectWallet, + } = useContext(SwapSdkContext); + + return { + provider, + signer, + network, + chainId, + account, + balance, + connectWallet, + disconnectWallet, + }; +}; diff --git a/src/react/providers/swapSdkProvider.tsx b/src/react/providers/swapSdkProvider.tsx new file mode 100644 index 0000000..c03564f --- /dev/null +++ b/src/react/providers/swapSdkProvider.tsx @@ -0,0 +1,223 @@ +import type { JsonRpcSigner } from '@ethersproject/providers'; +import { ethers } from 'ethers'; +import { createContext, ReactNode, useEffect, useState } from 'react'; +import { NftSwapV4 } from '../../sdk'; + +declare global { + interface Window { + ethereum: any; + } +} + +export interface ISwapSdkConfig { + reloadOnNetworkChange?: boolean; + rerenderOnNetworkChange?: boolean; + reloadOnAccountChange?: boolean; + rerenderOnAccountChange?: boolean; +} + +interface ISwapSdkContext { + nftSwap?: NftSwapV4; + + provider?: ethers.providers.Web3Provider; + signer?: JsonRpcSigner; + network?: ethers.providers.Network; + chainId?: number; + account?: string; + balance?: ethers.BigNumber; + + connectWallet?: () => Promise; + disconnectWallet?: () => void; +} + +const INITIAL_VALUE = { + nftSwap: undefined, + + provider: undefined, + signer: undefined, + network: undefined, + chainId: undefined, + account: undefined, + balance: undefined, + + connectWallet: undefined, + disconnectWallet: undefined, +}; + +export const SwapSdkContext = createContext(INITIAL_VALUE); + +interface ISwapSdkProviderProps { + config?: ISwapSdkConfig; + children?: ReactNode; +} + +export const SwapSdkProvider = (props: ISwapSdkProviderProps) => { + const { config, children } = props; + + const [nftSwap, setNftSwap] = useState( + INITIAL_VALUE.nftSwap + ); + + const [provider, setProvider] = useState< + ethers.providers.Web3Provider | undefined + >(INITIAL_VALUE.provider); + const [signer, setSigner] = useState( + INITIAL_VALUE.signer + ); + const [network, setNetwork] = useState( + INITIAL_VALUE.network + ); + const [chainId, setChainId] = useState( + INITIAL_VALUE.chainId + ); + const [account, setAccount] = useState( + INITIAL_VALUE.account + ); + const [balance, setBalance] = useState( + INITIAL_VALUE.balance + ); + + const [rerender, setRerender] = useState(false); + + /** Connect browser wallet to dapp */ + const connectWallet = async () => { + if (!window) throw new Error('Window is undefined'); + + const web3Provider = new ethers.providers.Web3Provider( + window.ethereum, + 'any' + ); + await web3Provider.send('eth_requestAccounts', []); + setProvider(web3Provider); + + const web3Signer = web3Provider.getSigner(); + setSigner(web3Signer); + + const web3WalletAddress = await web3Signer.getAddress(); + setAccount(web3WalletAddress); + + const web3WalletBalance = await web3Signer.getBalance(); + setBalance(web3WalletBalance); + + const web3Network = web3Provider.network; + setNetwork(web3Network); + + const web3ChainId = await web3Signer.getChainId(); + setChainId(web3ChainId); + }; + + /** Disconnect browser wallet from dapp */ + const disconnectWallet = () => { + setProvider(undefined); + setSigner(undefined); + setAccount(undefined); + setBalance(undefined); + setNetwork(undefined); + setChainId(undefined); + setNftSwap(undefined); + }; + + /* Create Swap SDK instance */ + useEffect(() => { + if (!provider) { + console.warn('Swap SDK init: provider is undefined'); + return; + } + if (!signer) { + console.warn('Swap SDK init: signer is undefined'); + return; + } + if (!account) { + console.warn('Swap SDK init: wallet address is undefined'); + return; + } + + const nftSwapInstance = new NftSwapV4(provider, signer, chainId); + setNftSwap(nftSwapInstance); + }, [provider, signer, chainId, account, rerender]); + + /* Subscribe on network change event */ + useEffect(() => { + if (!provider) { + console.warn('Network change event handler: provider is undefined'); + return; + } + + provider.on('network', (newNetwork: any, oldNetwork: any) => { + if (!oldNetwork) return; + + setNetwork(newNetwork); + + if (!config) return; + + if (config.reloadOnNetworkChange) { + window.location.reload(); + } + if (config.rerenderOnNetworkChange) { + setRerender((prev) => !prev); + } + }); + }, [provider]); + + /* Subscribe on account change event */ + useEffect(() => { + if (!provider) { + console.warn('Account change event handler: provider is undefined'); + return; + } + + window.ethereum.on('accountsChanged', (accounts: string[]) => { + const newAccount = accounts[0]; + if (!newAccount) { + disconnectWallet(); + return; + } + + if (newAccount === account) { + return; + } + + setAccount(newAccount); + + if (!config) return; + + if (config.reloadOnAccountChange) { + window.location.reload(); + } + if (config.rerenderOnAccountChange) { + setRerender((prev) => !prev); + } + }); + }, [provider]); + + /* Subscribe on disconnect wallet event */ + useEffect(() => { + if (!provider) { + console.warn('Disconnect event handler: provider is undefined'); + return; + } + + window.ethereum.on('disconnect', () => disconnectWallet()); + }, [provider]); + + /** Defined values for context provider */ + const swapSdkProviderValue: ISwapSdkContext = { + nftSwap, + + provider, + signer, + network, + chainId, + account, + balance, + + connectWallet, + disconnectWallet, + }; + + return ( + + {children} + + ); +};