diff --git a/package-lock.json b/package-lock.json index 1ecc5bdda..6d2b9e3f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43080,6 +43080,7 @@ "cids": "^1.1.9", "dayjs": "1.11.7", "eth-revert-reason": "^1.0.3", + "ethers-v6": "npm:ethers@^6.10.0", "formik": "2.2.9", "graphql-request": "5.2.0", "jotai": "^1.13.1", @@ -43523,6 +43524,11 @@ "node": ">=18" } }, + "packages/react-kit/node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "packages/react-kit/node_modules/ajv": { "version": "8.12.0", "license": "MIT", @@ -43707,6 +43713,39 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "packages/react-kit/node_modules/ethers-v6": { + "name": "ethers", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.10.0.tgz", + "integrity": "sha512-nMNwYHzs6V1FR3Y4cdfxSQmNgZsRj1RiTU25JwvnJLmyzw9z3SKxNc2XKDuiXXo/v9ds5Mp9m6HBabgYQQ26tA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "packages/react-kit/node_modules/ethers-v6/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, "packages/react-kit/node_modules/extract-files": { "version": "9.0.0", "license": "MIT", @@ -43822,6 +43861,11 @@ "url": "https://opencollective.com/svgo" } }, + "packages/react-kit/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "packages/react-kit/node_modules/ua-parser-js": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", @@ -43844,6 +43888,26 @@ "node": "*" } }, + "packages/react-kit/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "packages/subgraph": { "name": "@bosonprotocol/subgraph", "version": "1.33.2", diff --git a/packages/react-kit/package.json b/packages/react-kit/package.json index 9ffc3c2d8..9987e06d5 100644 --- a/packages/react-kit/package.json +++ b/packages/react-kit/package.json @@ -43,6 +43,7 @@ "cids": "^1.1.9", "dayjs": "1.11.7", "eth-revert-reason": "^1.0.3", + "ethers-v6": "npm:ethers@^6.10.0", "formik": "2.2.9", "graphql-request": "5.2.0", "jotai": "^1.13.1", diff --git a/packages/react-kit/src/components/config/ConfigContext.ts b/packages/react-kit/src/components/config/ConfigContext.ts index 38c533d7a..25bb45138 100644 --- a/packages/react-kit/src/components/config/ConfigContext.ts +++ b/packages/react-kit/src/components/config/ConfigContext.ts @@ -1,6 +1,7 @@ import { ProtocolConfig } from "@bosonprotocol/core-sdk"; import { createContext, useContext } from "react"; import { Signer } from "ethers"; +import { Signer as SignerV6 } from "ethers-v6"; export type ConfigContextProps = { config: ProtocolConfig; @@ -17,6 +18,7 @@ export type ConfigContextProps = { externalConnectedChainId?: number; externalConnectedAccount?: string; externalConnectedSigner?: Signer; + externalConnectedSignerV6?: SignerV6; withExternalConnectionProps?: boolean; withWeb3React: boolean; withCustomReduxContext?: boolean; diff --git a/packages/react-kit/src/components/cta/offer/CreateListing.tsx b/packages/react-kit/src/components/cta/offer/CreateListing.tsx new file mode 100644 index 000000000..c19f0f2ef --- /dev/null +++ b/packages/react-kit/src/components/cta/offer/CreateListing.tsx @@ -0,0 +1,214 @@ +import React, { useState } from "react"; +import { BigNumberish } from "ethers"; +import { ChainId } from "@bosonprotocol/common"; +import { Chain as OSChain, OpenSeaSDK } from "opensea-js"; +import { Signer as SignerV6 } from "ethers-v6"; +import { API_BASE_MAINNET, API_BASE_TESTNET } from "opensea-js/lib/constants"; + +import { Button } from "../../buttons/Button"; +import { ButtonTextWrapper, ExtraInfo, LoadingWrapper } from "../common/styles"; +import { CtaButtonProps } from "../common/types"; +import { Loading } from "../../ui/loading/Loading"; +import { ButtonSize } from "../../ui/buttonSize"; +import { useCoreSdkOverrides } from "../../../hooks/core-sdk/useCoreSdkOverrides"; +import { withQueryClientProvider } from "../../queryClient/withQueryClientProvider"; +import { useSignerV6 } from "../../../hooks"; +import { + Listing, + MarketplaceType +} from "@bosonprotocol/core-sdk/dist/cjs/marketplaces/types"; +import { + ConfigProvider, + ConfigProviderProps +} from "../../config/ConfigProvider"; + +type Props = { + tokenId: BigNumberish; + price: BigNumberish; +} & CtaButtonProps<{ + tokenId: BigNumberish; + price: BigNumberish; +}> & { + providerProps: Omit; + }; + +function getOpenSeaChain(chainId: ChainId): OSChain { + switch (chainId) { + case 1: { + return OSChain.Mainnet; + } + case 137: { + return OSChain.Polygon; + } + case 80002: { + return OSChain.Amoy; + } + case 11155111: { + return OSChain.Sepolia; + } + case 31337: { + return "hardhat" as OSChain; + } + default: { + throw new Error(`Chain ${chainId} not supported`); + } + } +} + +function createOpenSeaSDK( + signerV6: SignerV6, + chainId: ChainId, + OPENSEA_API_KEY: string +): OpenSeaSDK { + let openseaUrl; + switch (chainId) { + case 1: + case 137: { + openseaUrl = API_BASE_MAINNET; + break; + } + default: { + openseaUrl = API_BASE_TESTNET; + } + } + const openseaSdk = new OpenSeaSDK( + signerV6 as any, + { + chain: getOpenSeaChain(chainId), + apiKey: OPENSEA_API_KEY, + apiBaseUrl: openseaUrl + }, + (line) => console.info(`SEPOLIA OS: ${line}`) + ); + (openseaSdk.api as any).apiBaseUrl = openseaUrl; // << force the API URL + return openseaSdk; +} + +const withConfigProvider = < + P extends { providerProps: Omit } +>( + WrappedComponent: React.ComponentType

+) => { + // Define the props type for the wrapped component + type Props = P; + + // Return a new component + const WithConfigProvider: React.FC = (props) => { + return ( + + + + ); + }; + + // Set display name for debugging purposes + const displayName = + WrappedComponent.displayName || WrappedComponent.name || "Component"; + WithConfigProvider.displayName = `withQueryClientProviderCustom(${displayName})`; + + return WithConfigProvider; +}; + +export const CreateListingButton = withConfigProvider( + withQueryClientProvider( + ({ + tokenId, + price, + disabled = false, + showLoading = false, + extraInfo, + onSuccess, + onError, + onPendingSignature, + onPendingTransaction, + waitBlocks = 1, + size = ButtonSize.Large, + variant = "secondaryFill", + children, + coreSdkConfig, + providerProps, + ...rest + }: Props) => { + const coreSdk = useCoreSdkOverrides({ coreSdkConfig }); + const [isLoading, setIsLoading] = useState(false); + const signerV6 = useSignerV6(); + const OPENSEA_API_KEY = "56a6c19ed53b48f3aab9eff2c2e91468"; + const OPENSEA_FEE_RECIPIENT = + "0x0000a26b00c1F0DF003000390027140000fAa719"; // On Real OpenSea + + return ( + + ); + } + ) +); diff --git a/packages/react-kit/src/components/cta/offer/PremintButton.tsx b/packages/react-kit/src/components/cta/offer/PremintButton.tsx new file mode 100644 index 000000000..db69f3038 --- /dev/null +++ b/packages/react-kit/src/components/cta/offer/PremintButton.tsx @@ -0,0 +1,153 @@ +import React, { useState } from "react"; +import { BigNumberish, providers } from "ethers"; +import { TransactionResponse } from "@bosonprotocol/common"; + +import { Button } from "../../buttons/Button"; +import { ButtonTextWrapper, ExtraInfo, LoadingWrapper } from "../common/styles"; +import { CtaButtonProps } from "../common/types"; +import { Loading } from "../../ui/loading/Loading"; +import { ButtonSize } from "../../ui/buttonSize"; +import { useCoreSdkOverrides } from "../../../hooks/core-sdk/useCoreSdkOverrides"; +import { useMetaTx } from "../../../hooks/useMetaTx"; +import { withQueryClientProvider } from "../../queryClient/withQueryClientProvider"; + +type Props = { + offerId: BigNumberish; + reserveLength: BigNumberish; + preMintAmount: BigNumberish; +} & CtaButtonProps<{ + offerId: BigNumberish; + reserveLength: BigNumberish; + preMintAmount: BigNumberish; +}>; + +export const PremintButton = withQueryClientProvider( + ({ + offerId, + reserveLength, + preMintAmount, + disabled = false, + showLoading = false, + extraInfo, + onSuccess, + onError, + onPendingSignature, + onPendingTransaction, + waitBlocks = 1, + size = ButtonSize.Large, + variant = "secondaryFill", + children, + coreSdkConfig, + ...rest + }: Props) => { + const coreSdk = useCoreSdkOverrides({ coreSdkConfig }); + const [isLoading, setIsLoading] = useState(false); + + const { isMetaTx } = useMetaTx(coreSdk); + + return ( + + ); + } +); diff --git a/packages/react-kit/src/components/magicLink/MagicContext.tsx b/packages/react-kit/src/components/magicLink/MagicContext.tsx index 54e0c1b60..28a28b5c8 100644 --- a/packages/react-kit/src/components/magicLink/MagicContext.tsx +++ b/packages/react-kit/src/components/magicLink/MagicContext.tsx @@ -1,4 +1,5 @@ import { ethers } from "ethers"; +import { ethers as ethersV6 } from "ethers-v6"; import { Magic } from "magic-sdk"; import { createContext } from "react"; @@ -7,4 +8,5 @@ export const MagicContext = createContext<{ uuid: string; }; magicProvider: ethers.providers.Web3Provider; + magicProviderV6: ethersV6.BrowserProvider; } | null>(null); diff --git a/packages/react-kit/src/components/magicLink/MagicProvider.tsx b/packages/react-kit/src/components/magicLink/MagicProvider.tsx index 503fa56c2..8b30a7143 100644 --- a/packages/react-kit/src/components/magicLink/MagicProvider.tsx +++ b/packages/react-kit/src/components/magicLink/MagicProvider.tsx @@ -5,6 +5,7 @@ import { CONFIG } from "../../lib/config/config"; import { useConfigContext } from "../config/ConfigContext"; import type { getRpcUrls } from "../../lib/const/networks"; import { ethers } from "ethers"; +import { ethers as ethersV6 } from "ethers-v6"; import { MagicContext } from "./MagicContext"; export const MagicProvider = ({ children }: { children: ReactNode }) => { @@ -51,7 +52,10 @@ export const InnerMagicProvider = ({ return { magic: magic as typeof magic & { uuid: string }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - magicProvider: new ethers.providers.Web3Provider(magic.rpcProvider as any) + magicProvider: new ethers.providers.Web3Provider( + magic.rpcProvider as any + ), + magicProviderV6: new ethersV6.BrowserProvider(magic.rpcProvider as any) }; // return magic provider too }, [chainId, magicLinkKey, rpcUrls]); return ( diff --git a/packages/react-kit/src/hooks/connection/connection.ts b/packages/react-kit/src/hooks/connection/connection.ts index 13972b647..73f970d7b 100644 --- a/packages/react-kit/src/hooks/connection/connection.ts +++ b/packages/react-kit/src/hooks/connection/connection.ts @@ -3,13 +3,15 @@ import { useMemo } from "react"; import { useNetwork, useAccount as useWagmiAccount } from "wagmi"; +import { Signer as SignerV6 } from "ethers-v6"; import { useUser } from "../../components/magicLink/UserContext"; import { useIsMagicLoggedIn, useMagicChainId, - useMagicProvider + useMagicProvider, + useMagicSignerV6 } from "../magic"; -import { useEthersSigner } from "../ethers/useEthersSigner"; +import { useEthersSigner, useEthersSignerV6 } from "../ethers/useEthersSigner"; import { useConfigContext } from "../../components/config/ConfigContext"; import { useExternalSigner } from "../../components/signer/useExternalSigner"; import { useSignerAddress } from "../useSignerAddress"; @@ -172,6 +174,33 @@ export function useSigner(): Signer | undefined { return signer; } +export function useSignerV6(): SignerV6 | undefined { + const { withExternalConnectionProps, externalConnectedSignerV6 } = + useConfigContext(); + let wagmiSigner: unknown, error: unknown; + try { + const { data: ethersSigner } = useEthersSignerV6(); + wagmiSigner = ethersSigner; + } catch (wagmiError) { + error = wagmiError; // error if the provider is not there + } + const { data: magicSignerV6 } = useMagicSignerV6(); + + const isMagicLoggedIn = useIsMagicLoggedIn(); + + const signer = useMemo(() => { + return externalConnectedSignerV6 + ? externalConnectedSignerV6 + : isMagicLoggedIn + ? (magicSignerV6 as SignerV6) + : (wagmiSigner as SignerV6); + }, [externalConnectedSignerV6, wagmiSigner, magicSignerV6, isMagicLoggedIn]); + if (!withExternalConnectionProps && error) { + throw error; + } + return signer; +} + export function useBalance( blockTag: Parameters< NonNullable>["getBalance"] diff --git a/packages/react-kit/src/hooks/ethers/useEthersSigner.ts b/packages/react-kit/src/hooks/ethers/useEthersSigner.ts index 5c6a3ed75..ea9b76fb0 100644 --- a/packages/react-kit/src/hooks/ethers/useEthersSigner.ts +++ b/packages/react-kit/src/hooks/ethers/useEthersSigner.ts @@ -1,5 +1,7 @@ import { providers } from "ethers"; +import { BrowserProvider, Signer as SignerV6 } from "ethers-v6"; import * as React from "react"; +import { useQuery } from "react-query"; import { type WalletClient, useWalletClient } from "wagmi"; export function walletClientToSigner(walletClient: WalletClient) { @@ -22,3 +24,27 @@ export function useEthersSigner({ chainId }: { chainId?: number } = {}) { [walletClient] ); } + +export async function walletClientToSignerV6( + walletClient: WalletClient +): Promise { + const { account, chain, transport } = walletClient; + const network = { + chainId: chain.id, + name: chain.name, + ensAddress: chain.contracts?.ensRegistry?.address + }; + const provider = new BrowserProvider(transport, network); + const signer = provider.getSigner(account.address); + return signer; +} + +/** Hook to convert a viem Wallet Client to an ethers.js Signer. */ +export function useEthersSignerV6({ chainId }: { chainId?: number } = {}) { + const { data: walletClient } = useWalletClient({ chainId }); + return useQuery(["signer", walletClient], async () => { + return walletClient + ? await walletClientToSignerV6(walletClient) + : undefined; + }); +} diff --git a/packages/react-kit/src/hooks/magic.ts b/packages/react-kit/src/hooks/magic.ts index 86fcaa9ab..9503c227a 100644 --- a/packages/react-kit/src/hooks/magic.ts +++ b/packages/react-kit/src/hooks/magic.ts @@ -30,6 +30,28 @@ export function useMagicProvider() { return context.magicProvider; } +export function useMagicProviderV6() { + const context = useContext(MagicContext); + if (!context) { + throw new Error("useMagic must be used within MagicContext"); + } + return context.magicProviderV6; +} + +export const useMagicSignerV6 = () => { + const context = useContext(MagicContext); + if (!context) { + throw new Error("useMagic must be used within MagicContext"); + } + return useQuery(["signer", context.magic?.uuid], async () => { + if (!context.magicProviderV6) { + return; + } + const signer = await context.magicProviderV6?.getSigner(); + return signer; + }); +}; + export function useMagicChainId() { const magicProvider = useMagicProvider(); return magicProvider?._network?.chainId; diff --git a/packages/react-kit/src/index.tsx b/packages/react-kit/src/index.tsx index e343a4c55..55da3b2a2 100644 --- a/packages/react-kit/src/index.tsx +++ b/packages/react-kit/src/index.tsx @@ -25,6 +25,8 @@ export * from "./components/cta/exchange/RevokeButton"; export * from "./components/cta/offer/CommitButton"; export * from "./components/cta/offer/VoidButton"; export * from "./components/cta/offer/BatchVoidButton"; +export * from "./components/cta/offer/PremintButton"; +export * from "./components/cta/offer/CreateListing"; export * from "./components/cta/offer/CreateOfferButton"; export * from "./components/cta/funds/DepositFundsButton"; export * from "./components/cta/funds/WithdrawFundsButton";