From fbe51ffcf358e823fabbc07dee1657c63adc8081 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Tue, 29 Mar 2022 18:32:00 +0200 Subject: [PATCH 01/12] feat: scaffold bridge form --- src/pages/app/bridge.tsx | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/pages/app/bridge.tsx diff --git a/src/pages/app/bridge.tsx b/src/pages/app/bridge.tsx new file mode 100644 index 0000000..dbebe64 --- /dev/null +++ b/src/pages/app/bridge.tsx @@ -0,0 +1,101 @@ +/* eslint-disable no-alert */ +import FormFieldError from 'components/forms/FormFieldError'; +import FormFieldInfo from 'components/forms/FormFieldInfo'; +import { Form, Formik } from 'formik'; +import useForceConnectMenu from 'hooks/useForceConnectMenu'; +import { NextPage } from 'next'; +import React from 'react'; +import { BridgeFormSchema } from 'utils/schemas'; + +const TestingMetadataPage: NextPage = () => { + useForceConnectMenu(); + + return ( + <> +
+
+
+
+
+ <> + alert(data)} + > + {(props) => ( +
+ <> +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+
+ +
+ +
+ )} +
+ +
+
+
+
+
+ + ); +}; + +export default TestingMetadataPage; From e2afac504587d69c6b43c0738771995f7228bb77 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Tue, 29 Mar 2022 18:33:04 +0200 Subject: [PATCH 02/12] feat: strict form schema --- src/pages/app/bridge.tsx | 2 +- src/utils/contracts.ts | 6 +++--- src/utils/schemas.ts | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pages/app/bridge.tsx b/src/pages/app/bridge.tsx index dbebe64..be18125 100644 --- a/src/pages/app/bridge.tsx +++ b/src/pages/app/bridge.tsx @@ -29,7 +29,7 @@ const TestingMetadataPage: NextPage = () => { tokenId: '', l1Receiver: '' }} - validationSchema={BridgeFormSchema} + validationSchema={BridgeFormSchema(chainIdNormalised, readonlyProvider)} onSubmit={(data) => alert(data)} > {(props) => ( 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') + }); From 43981f2e2cd7aaa10eee019e999601b10dd5f662 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Tue, 29 Mar 2022 18:35:12 +0200 Subject: [PATCH 03/12] refactor: split approval state into reducer --- src/components/Assets/ERC721Asset.tsx | 4 +- .../Collection/CollectionSpecificsInfo.tsx | 4 +- .../Collection/DataCollectionCard.tsx | 4 +- .../OrderManipulation/forms/SellForm.tsx | 4 +- .../OrderManipulation/states/Approve.tsx | 9 +- .../OrderManipulation/states/Cancel.tsx | 4 +- .../OrderManipulation/states/Exercise.tsx | 4 +- src/state/index.ts | 3 + src/state/reducers/approvals.ts | 110 ++++++++++++++++++ src/state/reducers/assets.ts | 4 +- src/state/reducers/collections.ts | 4 +- src/state/reducers/orders.ts | 55 +++------ 12 files changed, 149 insertions(+), 60 deletions(-) create mode 100644 src/state/reducers/approvals.ts 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/state/index.ts b/src/state/index.ts index 956635d..5175723 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,4 +1,5 @@ import { configureStore } from '@reduxjs/toolkit'; +import approvalsReducer, { approvalsSlice } from './reducers/approvals'; import assetsReducer, { assetsSlice } from './reducers/assets'; import collectionsReducer, { collectionsSlice } from './reducers/collections'; import ordersReducer, { ordersSlice } from './reducers/orders'; @@ -7,6 +8,7 @@ import userReducer, { userSlice } from './reducers/user'; export const store = configureStore({ reducer: { + approvals: approvalsReducer, assets: assetsReducer, collections: collectionsReducer, orders: ordersReducer, @@ -15,6 +17,7 @@ export const store = configureStore({ }, devTools: { actionCreators: { + ...approvalsSlice.actions, ...assetsSlice.actions, ...collectionsSlice.actions, ...ordersSlice.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/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; + } }); } }); From a27bb2d99875e0ada78eb4e22593de4116fb9d3f Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Tue, 29 Mar 2022 18:35:24 +0200 Subject: [PATCH 04/12] refactor: adjust bridge address' --- src/constants/contracts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants/contracts.ts b/src/constants/contracts.ts index ee9e8f9..99afb94 100644 --- a/src/constants/contracts.ts +++ b/src/constants/contracts.ts @@ -8,8 +8,8 @@ export const ERC721_EXCHANGE: { [K in SupportedChainId]: `0x${string}` } = { [SupportedChainId.BOBA_RINKEBY]: BOBA_RINKEBY_ERC721_EXCHANGE }; -const BOBA_MAINNET_L2_NFT_BRIDGE = '0xE791c5A8aC5299bb946226d6E4864022c982371b'; -const BOBA_RINKEBY_L2_NFT_BRIDGE = '0x9b175c83d6238cB4a48E6f3C025D43E35b04391f'; +const BOBA_MAINNET_L2_NFT_BRIDGE = '0xFB823b65D0Dc219fdC0d759172D1E098dA32f9eb'; +const BOBA_RINKEBY_L2_NFT_BRIDGE = '0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA'; export const L2_NFT_BRIDGE: { [K in SupportedChainId]: `0x${string}` } = { [SupportedChainId.BOBA]: BOBA_MAINNET_L2_NFT_BRIDGE, From 311b270bf123f2e1e5d102a51611700418941f22 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Tue, 29 Mar 2022 18:35:34 +0200 Subject: [PATCH 05/12] refactor: expand bridge form --- src/pages/app/bridge.tsx | 157 +++++++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 65 deletions(-) diff --git a/src/pages/app/bridge.tsx b/src/pages/app/bridge.tsx index be18125..65712cf 100644 --- a/src/pages/app/bridge.tsx +++ b/src/pages/app/bridge.tsx @@ -1,14 +1,26 @@ /* 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 { 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 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 ( <> @@ -19,76 +31,91 @@ const TestingMetadataPage: NextPage = () => { }} >
-
+
- <> - +
+
+
Boba
+
+ +
+
Ethereum
+
+
+
+
+ <> + alert(data)} - > - {(props) => ( -
- <> -
-
- - - - -
-
- - - - + onSubmit={(data) => alert(data)} + > + {(props) => ( + + <> +
+
+ + + + +
+
+ + + + +
+
+ + + + +
-
- - - - +
+ + +
-
-
- -
- - - )} - - + + + )} + + +
From 178f332d87225ec818b14ab7399fb04488f5367a Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Wed, 30 Mar 2022 21:27:45 +0200 Subject: [PATCH 06/12] refactor: adjust bridge UI colour use --- src/pages/app/bridge.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/pages/app/bridge.tsx b/src/pages/app/bridge.tsx index 65712cf..618e0d1 100644 --- a/src/pages/app/bridge.tsx +++ b/src/pages/app/bridge.tsx @@ -34,14 +34,20 @@ const TestingMetadataPage: NextPage = () => {
-
+
-
Boba
+
+ + Boba +
-
Ethereum
+
+ + Ethereum +
@@ -70,7 +76,7 @@ const TestingMetadataPage: NextPage = () => { type="text" onChange={props.handleChange} value={props.values.l2Contract} - className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 text-white" + className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 py-1 text-white" />
@@ -84,7 +90,7 @@ const TestingMetadataPage: NextPage = () => { type="text" onChange={props.handleChange} value={props.values.tokenId} - className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 pl-1 text-white" + className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 py-1 text-white" />
@@ -98,14 +104,17 @@ const TestingMetadataPage: NextPage = () => { type="text" onChange={props.handleChange} value={props.values.l1Receiver} - className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 pl-1 text-white" + className="border-element-secondary outline-none block w-full rounded-lg border-b border-dotted bg-darks-200 px-2 py-1 text-white" />
- From 530440e3c0472bda5366125f44b06ddaf3d424db Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Wed, 30 Mar 2022 22:05:27 +0200 Subject: [PATCH 07/12] feat: adjust meta tag title on bridge ui --- README.md | 8 +------- src/next.config.js | 4 ++++ src/pages/app/bridge.tsx | 8 ++++++-- src/pages/app/testing/metadata.tsx | 2 ++ .../assets/chains/{mainnet.svg => ethereum.svg} | 0 .../assets/chains/{mainnet.webp => ethereum.webp} | Bin 6 files changed, 13 insertions(+), 9 deletions(-) rename src/public/assets/chains/{mainnet.svg => ethereum.svg} (100%) rename src/public/assets/chains/{mainnet.webp => ethereum.webp} (100%) 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/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 index 618e0d1..1d14350 100644 --- a/src/pages/app/bridge.tsx +++ b/src/pages/app/bridge.tsx @@ -3,6 +3,7 @@ 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'; @@ -10,6 +11,7 @@ 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'; @@ -24,6 +26,8 @@ const TestingMetadataPage: NextPage = () => { return ( <> + +
{
- + Boba L2 logo Boba
- + Ethereum logo Ethereum
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 From d1455342f6d29cb3cb4f661e2e38d79fd81146f2 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Wed, 6 Apr 2022 22:40:18 +0200 Subject: [PATCH 08/12] refactor: log tx error to browser console --- src/state/reducers/transactions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 14fa328fd5987eadd8fd0535b788d07f7388f0cf Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Thu, 7 Apr 2022 00:39:10 +0200 Subject: [PATCH 09/12] feat: create basic withdrawal to l1 vercel deploy might fail due to missing reducer --- package.json | 2 +- src/state/index.ts | 3 ++ src/state/reducers/bridging.ts | 66 ++++++++++++++++++++++++++++++++++ yarn.lock | 8 ++--- 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/state/reducers/bridging.ts diff --git a/package.json b/package.json index 32380dd..662a883 100644 --- a/package.json +++ b/package.json @@ -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/state/index.ts b/src/state/index.ts index 5175723..eaabecc 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,6 +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'; @@ -10,6 +11,7 @@ export const store = configureStore({ reducer: { approvals: approvalsReducer, assets: assetsReducer, + bridging: bridgingReducer, collections: collectionsReducer, orders: ordersReducer, transactions: transactionsReducer, @@ -19,6 +21,7 @@ export const store = configureStore({ actionCreators: { ...approvalsSlice.actions, ...assetsSlice.actions, + ...bridgingSlice.actions, ...collectionsSlice.actions, ...ordersSlice.actions, ...transactionsSlice.actions, 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/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" From e0ff98f3f23bbf4b8e381d1251dda9ca721b2238 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sat, 9 Apr 2022 19:38:03 +0200 Subject: [PATCH 10/12] fix: update nft bridge contract address --- src/constants/contracts.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/constants/contracts.ts b/src/constants/contracts.ts index 99afb94..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 = '0xFB823b65D0Dc219fdC0d759172D1E098dA32f9eb'; -const BOBA_RINKEBY_L2_NFT_BRIDGE = '0x5E368E9dce71B624D7DdB155f360E7A4969eB7aA'; +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, From ec23e99f5d5060cc84017f12628f2dfb834dd54b Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sat, 23 Apr 2022 22:25:40 +0200 Subject: [PATCH 11/12] fix: use proper suffix --- src/components/Assets/DataAssetCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: { From d46e0fd42d9be07fe0cd91fff5e2075d13812922 Mon Sep 17 00:00:00 2001 From: Nejc Drobnic Date: Sat, 23 Apr 2022 22:25:58 +0200 Subject: [PATCH 12/12] chore: adjust version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 662a883..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",