diff --git a/README.md b/README.md index 039b528..9a2e2e3 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,4 @@ ## Subgraphs -- Ethereum Mainnet -- Ethereum Rinkeby -- Boba Mainnet - - [EIP721](https://graph.mainnet.boba.network:8000/subgraphs/name/tapioca/eip721-subgraph-boba) -- Boba Rinkeby - - [EIP721](https://graph.rinkeby.boba.network:8000/subgraphs/name/tapioca/eip721-subgraph-boba) - - [ERC721ExchangeUpgradeable](https://graph.rinkeby.boba.network:8000/subgraphs/name/shibuidao/nft-exchange) +Information about our subgraph can be found [here](https://docs.shibuidao.com/nft/subgraph/Exchange.html). diff --git a/package.json b/package.json index 32380dd..88e10e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shibuidao/interface", - "version": "0.2.0-beta", + "version": "0.3.0-beta", "private": true, "scripts": { "predev": "yarn meta:package", @@ -67,7 +67,7 @@ "@sapphire/eslint-config": "4.0.8", "@sapphire/prettier-config": "1.2.7", "@sapphire/ts-config": "3.1.6", - "@shibuidao/boba-nft-bridge": "^1.1.0", + "@shibuidao/boba-nft-bridge": "^1.1.1", "@shibuidao/exchange": "^1.6.0", "@storybook/addon-actions": "^6.4.19", "@storybook/addon-controls": "^6.4.19", diff --git a/src/components/Assets/DataAssetCard.tsx b/src/components/Assets/DataAssetCard.tsx index cc53e5b..6a65b4b 100644 --- a/src/components/Assets/DataAssetCard.tsx +++ b/src/components/Assets/DataAssetCard.tsx @@ -5,7 +5,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { selectAssetMetadata } from 'state/reducers/assets'; -import { executeSellOrder, OrderDirection, selectSellOrder, setCurrentOrder, SimpleSellOrder } from 'state/reducers/orders'; +import { executeSellOrderTxw, OrderDirection, selectSellOrder, setCurrentOrder, SimpleSellOrder } from 'state/reducers/orders'; import AssetCard from './AssetCard'; export interface DataAssetCardProps { @@ -37,7 +37,7 @@ const DataAssetCard: React.FC = ({ chainId, contract, identi }; const exerciseFunction = (order: SimpleSellOrder, chainId_: SupportedChainId, library_: JsonRpcProvider, account_: string) => () => { dispatch( - executeSellOrder({ + executeSellOrderTxw({ chainId: chainId_, library: library_, data: { diff --git a/src/components/Assets/ERC721Asset.tsx b/src/components/Assets/ERC721Asset.tsx index 2d5088e..9b7122f 100644 --- a/src/components/Assets/ERC721Asset.tsx +++ b/src/components/Assets/ERC721Asset.tsx @@ -5,7 +5,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useProviders from 'hooks/useProviders'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchAssetMetadata, selectAssetMetadata } from 'state/reducers/assets'; +import { fetchAssetMetadataTxr, selectAssetMetadata } from 'state/reducers/assets'; import DataAssetCard from './DataAssetCard'; export interface ERC721AssetProps { @@ -24,7 +24,7 @@ const ERC721Asset: React.FC = ({ token, chainId }) => { if (metadata !== undefined && metadata.owner === token.owner.id) return; dispatch( - fetchAssetMetadata({ + fetchAssetMetadataTxr({ token: { owner: token.owner?.id, identifier: token.identifier, diff --git a/src/components/Collection/CollectionSpecificsInfo.tsx b/src/components/Collection/CollectionSpecificsInfo.tsx index 2ff4064..78db382 100644 --- a/src/components/Collection/CollectionSpecificsInfo.tsx +++ b/src/components/Collection/CollectionSpecificsInfo.tsx @@ -5,7 +5,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useProviders from 'hooks/useProviders'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchCollectionInfo, selectCollectionInfo } from 'state/reducers/collections'; +import { fetchCollectionInfoTxr, selectCollectionInfo } from 'state/reducers/collections'; export interface CollectionSpecificsInfoProps { address: string; @@ -20,7 +20,7 @@ const CollectionSpecificsInfo: React.FC = ({ addre const info = useSelector(selectCollectionInfo(chainIdNormalised, address)); if (!info) dispatch( - fetchCollectionInfo({ + fetchCollectionInfoTxr({ address, chainId: chainIdNormalised, provider: account && library ? library : baseProvider, diff --git a/src/components/Collection/DataCollectionCard.tsx b/src/components/Collection/DataCollectionCard.tsx index e51ea67..f3d1080 100644 --- a/src/components/Collection/DataCollectionCard.tsx +++ b/src/components/Collection/DataCollectionCard.tsx @@ -6,7 +6,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useProviders from 'hooks/useProviders'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchCollectionInfo, selectCollectionInfo } from 'state/reducers/collections'; +import { fetchCollectionInfoTxr, selectCollectionInfo } from 'state/reducers/collections'; import CollectionCard from './CollectionCard'; export interface DataCollectionCardProps { @@ -22,7 +22,7 @@ const DataCollectionCard: React.FC = ({ address }) => { const info = useSelector(selectCollectionInfo(chainIdNormalised, address)); if (!info) dispatch( - fetchCollectionInfo({ + fetchCollectionInfoTxr({ address, chainId: chainIdNormalised, provider: account && library ? library : baseProvider, diff --git a/src/components/OrderManipulation/forms/SellForm.tsx b/src/components/OrderManipulation/forms/SellForm.tsx index 4a7ec6b..5e0aa4e 100644 --- a/src/components/OrderManipulation/forms/SellForm.tsx +++ b/src/components/OrderManipulation/forms/SellForm.tsx @@ -6,7 +6,7 @@ import { Form, Formik } from 'formik'; import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import React from 'react'; import { useDispatch } from 'react-redux'; -import { clearOrder, createSellOrder } from 'state/reducers/orders'; +import { clearOrder, createSellOrderTxw } from 'state/reducers/orders'; import { SellFormSchema } from 'utils/schemas'; export interface SellFormFields { @@ -35,7 +35,7 @@ const SellForm: React.FC = ({ contract, identifier }) => { validationSchema={SellFormSchema} onSubmit={(values: SellFormFields) => { dispatch( - createSellOrder({ + createSellOrderTxw({ chainId, library, data: { diff --git a/src/components/OrderManipulation/states/Approve.tsx b/src/components/OrderManipulation/states/Approve.tsx index 438487d..29e9a62 100644 --- a/src/components/OrderManipulation/states/Approve.tsx +++ b/src/components/OrderManipulation/states/Approve.tsx @@ -5,7 +5,8 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useProviders from 'hooks/useProviders'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchApprovalStatus, OrderDirection, selectOrderingStatus, setApprovalForAll, updateCurrentOrderDirection } from 'state/reducers/orders'; +import { setApprovalForAllTxw } from 'state/reducers/approvals'; +import { fetchCurrentOrderApprovalStatusTxr, OrderDirection, selectOrderingStatus, updateCurrentOrderDirection } from 'state/reducers/orders'; const Approve: React.FC = () => { const { library, account, chainId } = useActiveWeb3React(); @@ -19,7 +20,7 @@ const Approve: React.FC = () => { if (!library || !account) return; dispatch( - fetchApprovalStatus({ + fetchCurrentOrderApprovalStatusTxr({ contract: order.contract, operator: ERC721_EXCHANGE[chainIdNormalised], owner: account, @@ -36,9 +37,11 @@ const Approve: React.FC = () => { disabled={!library} onClick={() => { dispatch( - setApprovalForAll({ + setApprovalForAllTxw({ contract: order.contract, + user: account!, operator: ERC721_EXCHANGE[chainIdNormalised], + approval: true, provider: library! }) ); diff --git a/src/components/OrderManipulation/states/Cancel.tsx b/src/components/OrderManipulation/states/Cancel.tsx index 9623ecb..9b69158 100644 --- a/src/components/OrderManipulation/states/Cancel.tsx +++ b/src/components/OrderManipulation/states/Cancel.tsx @@ -3,7 +3,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useMounted from 'hooks/useMounted'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { cancelSellOrder, clearOrder, OrderDirection, selectOrderingStatus } from 'state/reducers/orders'; +import { cancelSellOrderTxw, clearOrder, OrderDirection, selectOrderingStatus } from 'state/reducers/orders'; const Cancel: React.FC = () => { const { library, chainId } = useActiveWeb3React(); @@ -16,7 +16,7 @@ const Cancel: React.FC = () => { if (!mounted || !chainId || !library) return; dispatch( - cancelSellOrder({ + cancelSellOrderTxw({ chainId, library: library!, data: { diff --git a/src/components/OrderManipulation/states/Exercise.tsx b/src/components/OrderManipulation/states/Exercise.tsx index 296561d..1a485b6 100644 --- a/src/components/OrderManipulation/states/Exercise.tsx +++ b/src/components/OrderManipulation/states/Exercise.tsx @@ -3,7 +3,7 @@ import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import useMounted from 'hooks/useMounted'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { clearOrder, executeSellOrder, OrderDirection, selectOrderingStatus } from 'state/reducers/orders'; +import { clearOrder, executeSellOrderTxw, OrderDirection, selectOrderingStatus } from 'state/reducers/orders'; const Exercise: React.FC = () => { const { library, account, chainId } = useActiveWeb3React(); @@ -16,7 +16,7 @@ const Exercise: React.FC = () => { if (!mounted || !chainId || !library) return; dispatch( - executeSellOrder({ + executeSellOrderTxw({ chainId, library: library!, data: { diff --git a/src/constants/contracts.ts b/src/constants/contracts.ts index ee9e8f9..d2317ed 100644 --- a/src/constants/contracts.ts +++ b/src/constants/contracts.ts @@ -1,15 +1,23 @@ import { SupportedChainId } from './chains'; -const BOBA_MAINNET_ERC721_EXCHANGE = '0x02af48a420f0934ecfc1c34f6da83db1e3e56af7'; -const BOBA_RINKEBY_ERC721_EXCHANGE = '0x34755A949E68b18F585eB91711351b697C1563d5'; +const BOBA_MAINNET_ERC721_EXCHANGE = '0x02af48a420f0934ecfc1c34f6da83db1e3e56af7'; // Proxy +const BOBA_RINKEBY_ERC721_EXCHANGE = '0x34755A949E68b18F585eB91711351b697C1563d5'; // Proxy export const ERC721_EXCHANGE: { [K in SupportedChainId]: `0x${string}` } = { [SupportedChainId.BOBA]: BOBA_MAINNET_ERC721_EXCHANGE, [SupportedChainId.BOBA_RINKEBY]: BOBA_RINKEBY_ERC721_EXCHANGE }; -const BOBA_MAINNET_L2_NFT_BRIDGE = '0xE791c5A8aC5299bb946226d6E4864022c982371b'; -const BOBA_RINKEBY_L2_NFT_BRIDGE = '0x9b175c83d6238cB4a48E6f3C025D43E35b04391f'; +const BOBA_MAINNET_L1_NFT_BRIDGE = '0xC891F466e53f40603250837282eAE4e22aD5b088'; // Proxy +const BOBA_RINKEBY_L1_NFT_BRIDGE = '0x01F5d5D6de3a8c7A157B22FD331A1F177b7bE043'; // Proxy + +export const L1_NFT_BRIDGE: { [K in SupportedChainId]: `0x${string}` } = { + [SupportedChainId.BOBA]: BOBA_MAINNET_L1_NFT_BRIDGE, + [SupportedChainId.BOBA_RINKEBY]: BOBA_RINKEBY_L1_NFT_BRIDGE +}; + +const BOBA_MAINNET_L2_NFT_BRIDGE = '0xFB823b65D0Dc219fdC0d759172D1E098dA32f9eb'; // Proxy +const BOBA_RINKEBY_L2_NFT_BRIDGE = '0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA'; // Proxy export const L2_NFT_BRIDGE: { [K in SupportedChainId]: `0x${string}` } = { [SupportedChainId.BOBA]: BOBA_MAINNET_L2_NFT_BRIDGE, diff --git a/src/next.config.js b/src/next.config.js index 95424f4..5ceb869 100644 --- a/src/next.config.js +++ b/src/next.config.js @@ -14,6 +14,10 @@ module.exports = withPlausibleProxy()({ { source: '/app', destination: '/app/collections' + }, + { + source: '/app/collections/index', + destination: '/app/collections' } ]; }, diff --git a/src/pages/app/bridge.tsx b/src/pages/app/bridge.tsx new file mode 100644 index 0000000..1d14350 --- /dev/null +++ b/src/pages/app/bridge.tsx @@ -0,0 +1,141 @@ +/* eslint-disable no-alert */ +import { faArrowRightLong } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import FormFieldError from 'components/forms/FormFieldError'; +import FormFieldInfo from 'components/forms/FormFieldInfo'; +import Offset from 'components/Navbar/Offset'; +import { SupportedChainId } from 'constants/chains'; +import { DEFAULT_CHAIN } from 'constants/misc'; +import { Form, Formik } from 'formik'; +import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; +import useForceConnectMenu from 'hooks/useForceConnectMenu'; +import useProviders from 'hooks/useProviders'; +import { NextPage } from 'next'; +import { NextSeo } from 'next-seo'; +import React from 'react'; +import { When } from 'react-if'; +import { BridgeFormSchema } from 'utils/schemas'; + +const TestingMetadataPage: NextPage = () => { + useForceConnectMenu(); + const { account, chainId, library } = useActiveWeb3React(); + + const chainIdNormalised: SupportedChainId = chainId || DEFAULT_CHAIN; + const fallbackProvider = useProviders()[chainIdNormalised]; + const readonlyProvider = library || fallbackProvider; + + return ( + <> + + +
+
+
+
+
+
+
+
+
+ Boba L2 logo + Boba +
+
+ +
+
+ Ethereum logo + Ethereum +
+
+
+
+
+ <> + alert(data)} + > + {(props) => ( +
+ <> +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ + + +
+ +
+ )} +
+ +
+
+
+
+
+
+ + ); +}; + +export default TestingMetadataPage; diff --git a/src/pages/app/testing/metadata.tsx b/src/pages/app/testing/metadata.tsx index 352cd88..52bf1c8 100644 --- a/src/pages/app/testing/metadata.tsx +++ b/src/pages/app/testing/metadata.tsx @@ -6,6 +6,7 @@ import { DEFAULT_CHAIN } from 'constants/misc'; import { Form, Formik } from 'formik'; import { useActiveWeb3React } from 'hooks/useActiveWeb3React'; import { NextPage } from 'next'; +import { NextSeo } from 'next-seo'; import Highlight, { defaultProps } from 'prism-react-renderer'; import React, { useState } from 'react'; import { useSelector } from 'react-redux'; @@ -21,6 +22,7 @@ const TestingMetadataPage: NextPage = () => { return ( <> +
diff --git a/src/public/assets/chains/mainnet.svg b/src/public/assets/chains/ethereum.svg similarity index 100% rename from src/public/assets/chains/mainnet.svg rename to src/public/assets/chains/ethereum.svg diff --git a/src/public/assets/chains/mainnet.webp b/src/public/assets/chains/ethereum.webp similarity index 100% rename from src/public/assets/chains/mainnet.webp rename to src/public/assets/chains/ethereum.webp diff --git a/src/state/index.ts b/src/state/index.ts index 956635d..eaabecc 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,5 +1,7 @@ import { configureStore } from '@reduxjs/toolkit'; +import approvalsReducer, { approvalsSlice } from './reducers/approvals'; import assetsReducer, { assetsSlice } from './reducers/assets'; +import bridgingReducer, { bridgingSlice } from './reducers/bridging'; import collectionsReducer, { collectionsSlice } from './reducers/collections'; import ordersReducer, { ordersSlice } from './reducers/orders'; import transactionsReducer, { transactionsSlice } from './reducers/transactions'; @@ -7,7 +9,9 @@ import userReducer, { userSlice } from './reducers/user'; export const store = configureStore({ reducer: { + approvals: approvalsReducer, assets: assetsReducer, + bridging: bridgingReducer, collections: collectionsReducer, orders: ordersReducer, transactions: transactionsReducer, @@ -15,7 +19,9 @@ export const store = configureStore({ }, devTools: { actionCreators: { + ...approvalsSlice.actions, ...assetsSlice.actions, + ...bridgingSlice.actions, ...collectionsSlice.actions, ...ordersSlice.actions, ...transactionsSlice.actions, diff --git a/src/state/reducers/approvals.ts b/src/state/reducers/approvals.ts new file mode 100644 index 0000000..f408d76 --- /dev/null +++ b/src/state/reducers/approvals.ts @@ -0,0 +1,110 @@ +import { Provider, StaticJsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ABI, ABIs } from 'constants/abis'; +import { Contract, errors } from 'ethers'; +import { RootState } from 'state'; +import { TRANSACTION_THRUNK_PREFIX } from './transactions'; + +export interface ApprovalsState { + approved: { [C: string]: { [U: string]: string[] | undefined } | undefined }; +} + +const initialState: ApprovalsState = { + approved: {} +}; + +export interface FetchContractApprovalParameters { + contract: string; + user: string; + operator: string; + provider: Provider; +} + +export const fetchApprovalStatusTxr = createAsyncThunk( + 'fetch/contract/approval', + async ({ contract, user, operator, provider }) => { + const collection = new Contract(contract, ABIs[ABI.EIP721], provider); + + const isApproved: boolean = await collection.isApprovedForAll(user, operator); + return isApproved; + } +); + +export interface SetContractApprovalParameters { + contract: string; + user: string; + operator: string; + approval: boolean; + provider: StaticJsonRpcProvider; +} + +export const setApprovalForAllTxw = createAsyncThunk( + `${TRANSACTION_THRUNK_PREFIX}set/contract/approval`, + async ({ contract, operator, provider, approval }, { rejectWithValue }) => { + const collection = new Contract(contract, ABIs[ABI.EIP721], provider.getSigner()); + + try { + const tx: TransactionResponse = await collection.setApprovalForAll(operator, approval); + + try { + await tx.wait(); + } catch (callException: any) { + if (callException.code === errors.CALL_EXCEPTION) { + return rejectWithValue(['Transaction execution failed', callException]); + } + throw callException; + } + } catch (transactionError) { + return rejectWithValue(['Method call failed', transactionError]); + } + + return true; + } +); + +export interface ApprovalForAllSetPayload { + contract: string; + user: string; + operator: string; + approval: boolean; +} + +// TODO: this way of storing data isn't chain agnostic +export const approvalsSlice = createSlice({ + name: 'approvals', + initialState, + reducers: { + setApprovalForAll: (state, action: PayloadAction) => { + state.approved[action.payload.contract] ??= {}; + const operators = new Set(state.approved[action.payload.contract]![action.payload.user] || []); + + action.payload.approval ? operators.add(action.payload.operator) : operators.delete(action.payload.operator); + state.approved[action.payload.contract]![action.payload.user] = [...operators.keys()]; + } + }, + extraReducers: (builder) => { + builder + .addCase(fetchApprovalStatusTxr.fulfilled, (state, action) => { + state.approved[action.meta.arg.contract] ??= {}; + const operators = new Set(state.approved[action.meta.arg.contract]![action.meta.arg.user] || []); + + action.payload ? operators.add(action.meta.arg.operator) : operators.delete(action.meta.arg.operator); + state.approved[action.meta.arg.contract]![action.meta.arg.user] = [...operators.keys()]; + }) + .addCase(setApprovalForAllTxw.fulfilled, (state, action) => { + state.approved[action.meta.arg.contract] ??= {}; + const operators = new Set(state.approved[action.meta.arg.contract]![action.meta.arg.user] || []); + + action.meta.arg.approval ? operators.add(action.meta.arg.operator) : operators.delete(action.meta.arg.operator); + state.approved[action.meta.arg.contract]![action.meta.arg.user] = [...operators.keys()]; + }); + } +}); + +export const { setApprovalForAll } = approvalsSlice.actions; + +export const selectOperators = (contract: string, user: string) => (state: RootState) => (state.approvals.approved[contract] || {})[user] || []; +export const selectOperatorApproval = (contract: string, user: string, operator: string) => (state: RootState) => + ((state.approvals.approved[contract] || {})[user] || []).includes(operator); + +export default approvalsSlice.reducer; diff --git a/src/state/reducers/assets.ts b/src/state/reducers/assets.ts index c799034..0b82663 100644 --- a/src/state/reducers/assets.ts +++ b/src/state/reducers/assets.ts @@ -42,7 +42,7 @@ export interface AssetMetadataSetPayload { data: ExpandedChainedMetadata; } -export const fetchAssetMetadata = createAsyncThunk( +export const fetchAssetMetadataTxr = createAsyncThunk( 'fetch/metadata/asset', async ({ token, contractABI, chainId, provider }, { rejectWithValue, getState }) => { if (!chainId || !contractABI) return rejectWithValue('ChainId or contract not provided.'); @@ -95,7 +95,7 @@ export const assetsSlice = createSlice({ } }, extraReducers: (builder) => { - builder.addCase(fetchAssetMetadata.fulfilled, (state, action) => { + builder.addCase(fetchAssetMetadataTxr.fulfilled, (state, action) => { state.metadata[`${action.payload.chainId}-${action.payload.contract}-${action.payload.identifier}`] = action.payload; if (action.payload.rawContractName && action.payload.contract) state.contractNames[action.payload.contract] = action.payload.rawContractName; diff --git a/src/state/reducers/bridging.ts b/src/state/reducers/bridging.ts new file mode 100644 index 0000000..25860a6 --- /dev/null +++ b/src/state/reducers/bridging.ts @@ -0,0 +1,66 @@ +import { StaticJsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { SupportedChainId } from 'constants/chains'; +import { BigNumber, errors } from 'ethers'; +import { l2NFTBridgeContract } from '../../utils/contracts'; +import { TRANSACTION_THRUNK_PREFIX } from './transactions'; + +export interface BridgingState { + bridgeHistory: { contract: string; tokenId: string }[]; +} + +const initialState: BridgingState = { + bridgeHistory: [] +}; + +export interface WithdrawNFTParameters { + chainId: SupportedChainId; + l2Contract: string; + tokenId: string; + l1Receiver?: string; + l1Gas: number; + provider: StaticJsonRpcProvider; +} + +export const withdrawNFTTxw = createAsyncThunk( + `${TRANSACTION_THRUNK_PREFIX}execute/contract/l2tl1Bridge`, + async ({ chainId, l2Contract, tokenId, l1Receiver, l1Gas, provider }, { rejectWithValue }) => { + const bridge = l2NFTBridgeContract(chainId, provider.getSigner()); + + try { + const txT = l1Receiver + ? bridge.withdrawTo(l2Contract, l1Receiver, BigNumber.from(tokenId), l1Gas) + : bridge.withdraw(l2Contract, BigNumber.from(tokenId), l1Gas); + const tx: TransactionResponse = await txT; + + try { + await tx.wait(); + } catch (callException: any) { + if (callException.code === errors.CALL_EXCEPTION) { + return rejectWithValue(['Transaction execution failed', callException]); + } + throw callException; + } + } catch (transactionError) { + return rejectWithValue(['Method call failed', transactionError]); + } + + return true; + } +); + +// TODO: this way of storing data isn't chain agnostic +export const bridgingSlice = createSlice({ + name: 'brigding', + initialState, + // TODO: Reducer to set and clear history + reducers: {}, + extraReducers: (builder) => { + // TODO: Store "withdrawNFTTxw" in history + builder; + } +}); + +export const {} = bridgingSlice.actions; + +export default bridgingSlice.reducer; diff --git a/src/state/reducers/collections.ts b/src/state/reducers/collections.ts index dff7480..614c657 100644 --- a/src/state/reducers/collections.ts +++ b/src/state/reducers/collections.ts @@ -36,7 +36,7 @@ export interface CollectionInfoSetPayload { data: ChainedCollectionInfo; } -export const fetchCollectionInfo = createAsyncThunk( +export const fetchCollectionInfoTxr = createAsyncThunk( 'fetch/metadata/collection', async ({ address, contractABI, chainId, provider }, { rejectWithValue }) => { if (!address || !chainId || !contractABI) return rejectWithValue('ChainId or contract not provided.'); @@ -66,7 +66,7 @@ export const collectionsSlice = createSlice({ } }, extraReducers: (builder) => { - builder.addCase(fetchCollectionInfo.fulfilled, (state, action) => { + builder.addCase(fetchCollectionInfoTxr.fulfilled, (state, action) => { state.info[`${action.payload.chainId}-${action.payload.address}`] = action.payload; }); } diff --git a/src/state/reducers/orders.ts b/src/state/reducers/orders.ts index fc61278..40ce73d 100644 --- a/src/state/reducers/orders.ts +++ b/src/state/reducers/orders.ts @@ -1,5 +1,5 @@ import type { Provider } from '@ethersproject/providers'; -import { JsonRpcProvider, StaticJsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; +import { JsonRpcProvider, TransactionResponse } from '@ethersproject/providers'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { deepClone } from '@sapphire/utilities'; import { BuyOrder, SellOrder } from '@shibuidao/erc721exchange-types'; @@ -11,6 +11,7 @@ import { BigNumberish, Contract, errors } from 'ethers'; import { WritableDraft } from 'immer/dist/internal'; import { RootState } from 'state'; import { exchangeContract } from 'utils/contracts'; +import { setApprovalForAllTxw } from './approvals'; import { TRANSACTION_THRUNK_PREFIX } from './transactions'; export interface SimpleSellOrder { @@ -100,15 +101,15 @@ const commitBuyOrder = (state: WritableDraft, order: BuyOrder): Wri return state; }; -export interface FetchContractApprovalParameters { +export interface FetchContractOrderApprovalParameters { contract: string; owner: string; operator: string; provider: Provider; } -export const fetchApprovalStatus = createAsyncThunk( - 'fetch/contract/approval', +export const fetchCurrentOrderApprovalStatusTxr = createAsyncThunk( + 'fetch/order/approval', async ({ contract, owner, operator, provider }) => { const collection = new Contract(contract, ABIs[ABI.EIP721], provider); @@ -118,36 +119,6 @@ export const fetchApprovalStatus = createAsyncThunk( - `${TRANSACTION_THRUNK_PREFIX}set/contract/approval`, - async ({ contract, operator, provider }, { rejectWithValue }) => { - const collection = new Contract(contract, ABIs[ABI.EIP721], provider.getSigner()); - - try { - const tx: TransactionResponse = await collection.setApprovalForAll(operator, true); - - try { - await tx.wait(); - } catch (callException: any) { - if (callException.code === errors.CALL_EXCEPTION) { - return rejectWithValue(['Transaction execution failed', callException]); - } - throw callException; - } - } catch (transactionError) { - return rejectWithValue(['Method call failed', transactionError]); - } - - return true; - } -); - export interface SellOrderData { tokenContractAddress: string; tokenId: BigNumberish; @@ -162,7 +133,7 @@ export interface CreateOrderSellParameters { data: SellOrderData; } -export const createSellOrder = createAsyncThunk( +export const createSellOrderTxw = createAsyncThunk( `${TRANSACTION_THRUNK_PREFIX}create/order/sell`, async ({ chainId, library, data }, { rejectWithValue }) => { const exchange = exchangeContract(ERC721_EXCHANGE[chainId], library.getSigner()); @@ -203,7 +174,7 @@ export interface CancelOrderSellParameters { data: SellOrderCancellationData; } -export const cancelSellOrder = createAsyncThunk( +export const cancelSellOrderTxw = createAsyncThunk( `${TRANSACTION_THRUNK_PREFIX}cancel/order/sell`, async ({ chainId, library, data }, { rejectWithValue }) => { const exchange = exchangeContract(ERC721_EXCHANGE[chainId], library.getSigner()); @@ -242,7 +213,7 @@ export interface ExecuteOrderSellParameters { data: SellOrderExecutionData; } -export const executeSellOrder = createAsyncThunk( +export const executeSellOrderTxw = createAsyncThunk( `${TRANSACTION_THRUNK_PREFIX}execute/order/sell`, async ({ chainId, library, data }, { rejectWithValue }) => { const exchange = exchangeContract(ERC721_EXCHANGE[chainId], library.getSigner()); @@ -316,12 +287,14 @@ export const ordersSlice = createSlice({ // TODO: Use case outputs extraReducers: (builder) => { builder - .addCase(fetchApprovalStatus.fulfilled, (state, action) => { + .addCase(fetchCurrentOrderApprovalStatusTxr.fulfilled, (state, action) => { state.currentOrder.approved = action.payload; }) - .addCase(setApprovalForAll.fulfilled, (state) => { - state.currentOrder.approved = true; - state.currentOrder.direction = OrderDirection.BOOK; + .addCase(setApprovalForAllTxw.fulfilled, (state, action) => { + if (action.meta.arg.contract === state.currentOrder.contract) { + state.currentOrder.approved = true; + state.currentOrder.direction = OrderDirection.BOOK; + } }); } }); diff --git a/src/state/reducers/transactions.ts b/src/state/reducers/transactions.ts index 4b1d4d3..eb45d23 100644 --- a/src/state/reducers/transactions.ts +++ b/src/state/reducers/transactions.ts @@ -42,7 +42,7 @@ export const transactionsSlice = createSlice({ .addMatcher(isAllOf(isTransactionAction, isAnyOf(isRejected, isRejectedWithValue)), (state, action) => { state.pending = state.pending.filter((tx) => tx !== action.meta.requestId); toast.error('Transaction failed.'); - console.error(action.payload); + console.error('Transaction failed.', '\n', action.payload); }) .addMatcher(isAllOf(isTransactionAction, isAnyOf(isFulfilled)), (state, action) => { state.pending = state.pending.filter((tx) => tx !== action.meta.requestId); diff --git a/src/utils/contracts.ts b/src/utils/contracts.ts index 26e125a..b1d064a 100644 --- a/src/utils/contracts.ts +++ b/src/utils/contracts.ts @@ -1,4 +1,4 @@ -import { JsonRpcSigner } from '@ethersproject/providers'; +import { JsonRpcSigner, Provider } from '@ethersproject/providers'; import type { ERC721ExchangeUpgradeable } from '@shibuidao/exchange'; import { ABI, ABIs } from 'constants/abis'; import { SupportedChainId } from 'constants/chains'; @@ -10,6 +10,6 @@ export function exchangeContract(address: string, signer: JsonRpcSigner): ERC721 return new Contract(address, ABIs[ABI.ERC721_EXCHANGE], signer) as ERC721ExchangeUpgradeable; } -export function l2NFTBridgeContract(chainId: SupportedChainId, signer: JsonRpcSigner): L2NFTBridge { - return new Contract(L2_NFT_BRIDGE[chainId], ABIs[ABI.L2_NFT_BRIDGE], signer) as unknown as L2NFTBridge; +export function l2NFTBridgeContract(chainId: SupportedChainId, signerOrProvider: JsonRpcSigner | Provider): L2NFTBridge { + return new Contract(L2_NFT_BRIDGE[chainId], ABIs[ABI.L2_NFT_BRIDGE], signerOrProvider) as unknown as L2NFTBridge; } diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 4d496a4..c450af5 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -1,4 +1,8 @@ +import { Provider } from '@ethersproject/providers'; +import { SupportedChainId } from 'constants/chains'; +import { ZERO_ADDRESS } from 'constants/misc'; import * as Yup from 'yup'; +import { l2NFTBridgeContract } from './contracts'; export const SellFormSchema = Yup.object().shape({ price: Yup.string().required(), @@ -13,3 +17,29 @@ export const MetadataTestingParametersFormSchema = Yup.object().shape({ contract: Yup.string().required(), asset: Yup.string().required() }); + +export const BridgeFormSchema = (chainId: SupportedChainId, provider: Provider) => + Yup.object().shape({ + l2Contract: Yup.string() + .required('The L2 NFT Contract address is required') + .matches(/^0x[a-fA-F0-9]{40}$/, 'This is not a valid address') + .length(42, 'This is not a valid address') + .test('validPair', "This contract isn't registered with the bridge", async (address) => { + if (address && address.length === 42) { + const bridge = l2NFTBridgeContract(chainId, provider); + try { + const pairInfo = await bridge.pairNFTInfo(address); + if (pairInfo.every((f) => f === ZERO_ADDRESS || f === 0)) return false; + } catch { + return false; + } + } + + return true; + }), + tokenId: Yup.string().required('The token ID of the asset being bridged is required'), + l1Receiver: Yup.string() + .optional() + .matches(/^0x[a-fA-F0-9]{40}$/, 'This is not a valid address') + .length(42, 'This is not a valid address') + }); diff --git a/yarn.lock b/yarn.lock index b79cf65..d7f399e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2479,10 +2479,10 @@ resolved "https://registry.yarnpkg.com/@sapphire/utilities/-/utilities-3.3.0.tgz#62ff0a52cd86bd6169a94b2f217d72da6772a3cd" integrity sha512-wWESfB03elALhci3GjcacRh8pnK89Qe5AEKCQplKyTCKabWl64SAFw52hQBth2fMmJStgK1fr87aGhRZAB8DNA== -"@shibuidao/boba-nft-bridge@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@shibuidao/boba-nft-bridge/-/boba-nft-bridge-1.1.0.tgz#d27627b752bf7d942035bcdb79b472574b6b60d5" - integrity sha512-tiFzOUhv6Mvc6FHjQRnaB3mjR5hn3Mle+Oj6wa3yq0Rn1VGH5/i/MJww4bVfOORMFu/uSjvxVy52vFPn+x7xvA== +"@shibuidao/boba-nft-bridge@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@shibuidao/boba-nft-bridge/-/boba-nft-bridge-1.1.1.tgz#557889dbd08587059dfbbdab2a3b3bbe64cf608d" + integrity sha512-rgb4u+YBEloct/W3HhxgBZcJ3ENO+pVff9Eya2fZntxb7Rl/vyU4UAK03uO9TMPDUoufNb0CDTtQ9BBno3eM0A== dependencies: "@typechain/ethers-v5" "^10.0.0" ethers "^5.6.2"