From 058ae1a6768e0b597802ec13663de4532c98c4d2 Mon Sep 17 00:00:00 2001 From: Samuel Castleman <142861450+scastleman-immutable@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:24:51 +1000 Subject: [PATCH] feat: ID-1502: sample app examples for create seaport listing (#1690) --- .../SeaportCreateListing.tsx | 219 ++++++++++++++++++ .../SeaportCreateListingDefault.tsx | 85 +++++++ .../SeaportCreateListingExample.ts | 156 +++++++++++++ .../zkevm/EthSignTypedDataV4Examples/index.ts | 4 + 4 files changed, 464 insertions(+) create mode 100644 packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListing.tsx create mode 100644 packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingDefault.tsx create mode 100644 packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingExample.ts diff --git a/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListing.tsx b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListing.tsx new file mode 100644 index 0000000000..37e1502e81 --- /dev/null +++ b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListing.tsx @@ -0,0 +1,219 @@ +import { utils } from 'ethers'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { + Accordion, Alert, Form, Spinner, Stack, +} from 'react-bootstrap'; +import WorkflowButton from '@/components/WorkflowButton'; +import { RequestExampleProps } from '@/types'; +import { useImmutableProvider } from '@/context/ImmutableProvider'; +import { usePassportProvider } from '@/context/PassportProvider'; +import { + ActionType, SignableAction, SignablePurpose, +} from '@imtbl/orderbook'; + +type TokenType = 'NATIVE' | 'ERC20'; + +const { _TypedDataEncoder: typedDataEncoder } = utils; + +function SeaportCreateListing({ disabled, handleExampleSubmitted }: RequestExampleProps) { + const { orderbookClient } = useImmutableProvider(); + const { zkEvmProvider } = usePassportProvider(); + + const [walletAddress, setWalletAddress] = useState(''); + const [isBuldingTransaction, setIsBuildingTransaction] = useState(false); + const [transaction, setTransaction] = useState(); + const [transactionError, setSignMessageError] = useState(''); + + const [NFTContractAddress, setNFTContractAddress] = useState(''); + const [tokenContractAddress, setTokenContractAddress] = useState(''); + const [buyAmount, setBuyAmount] = useState(''); + const [buyType, setBuyType] = useState<'NATIVE' | 'ERC20'>('NATIVE'); + const [tokenId, setTokenId] = useState(''); + + const seaportContractAddress = useMemo( + () => ( + orderbookClient.config().seaportContractAddress + ), + [orderbookClient], + ); + + useEffect(() => { + const getAddress = async () => { + if (zkEvmProvider) { + const [address] = await zkEvmProvider.request({ + method: 'eth_requestAccounts', + }); + setWalletAddress(address || ''); + } + }; + + getAddress().catch(console.log); + }, [zkEvmProvider, setWalletAddress]); + + useEffect(() => { + setSignMessageError(''); + setTransaction(undefined); + }, [NFTContractAddress, tokenContractAddress, buyAmount, buyType, tokenId]); + + const validate = useCallback(async () => { + const buy = buyType === 'NATIVE' + ? { amount: buyAmount, type: buyType } + : { amount: buyAmount, type: buyType, contractAddress: tokenContractAddress }; + try { + setIsBuildingTransaction(true); + const { actions } = await orderbookClient.prepareListing({ + makerAddress: walletAddress, + buy, + sell: { + contractAddress: NFTContractAddress, + tokenId, + type: 'ERC721', + }, + }); + + const signAction = actions.find((action) => ( + action.type === ActionType.SIGNABLE && action.purpose === SignablePurpose.CREATE_LISTING + )) as SignableAction | undefined; + + const populated = await typedDataEncoder.resolveNames( + signAction!.message!.domain, + signAction!.message!.types, + signAction!.message!.value, + (name: string) => Promise.resolve(name), + ); + + const payload = typedDataEncoder.getPayload(populated.domain, signAction!.message!.types, populated.value); + setTransaction(payload); + } catch (err) { + setSignMessageError(`Failed to validate listing: ${err}`); + } finally { + setIsBuildingTransaction(false); + } + }, [NFTContractAddress, buyAmount, buyType, orderbookClient, tokenContractAddress, tokenId, walletAddress]); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + + return handleExampleSubmitted({ + method: 'eth_signTypedData_v4', + params: [walletAddress, transaction], + }); + }, [handleExampleSubmitted, transaction, walletAddress]); + + return ( + + Seaport Create Listing + + + Note: This method only returns a signed message, it does not submit an order to the orderbook. + + {transactionError + && ( + + {transactionError} + + )} +
+ + + Seaport Contract Address + + + + + + NFT Contract Address + + setNFTContractAddress(e.target.value)} + /> + + + + Token ID + + setTokenId(e.target.value)} + /> + + + + Currency Type + + setBuyType(e.target.value as TokenType)} + > + + + + + {buyType === 'ERC20' && ( + + + Currency Contract Address + + setTokenContractAddress(e.target.value)} + /> + + )} + + + Currency Amount + + setBuyAmount(e.target.value)} + /> + + + + Validate + + + Submit + + { isBuldingTransaction && } + +
+
+
+ ); +} + +export default SeaportCreateListing; diff --git a/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingDefault.tsx b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingDefault.tsx new file mode 100644 index 0000000000..76ca0508e2 --- /dev/null +++ b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingDefault.tsx @@ -0,0 +1,85 @@ +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { + Accordion, Alert, Form, Stack, +} from 'react-bootstrap'; +import WorkflowButton from '@/components/WorkflowButton'; +import { RequestExampleProps } from '@/types'; +import { useImmutableProvider } from '@/context/ImmutableProvider'; +import { usePassportProvider } from '@/context/PassportProvider'; +import { getCreateListingPayload } from './SeaportCreateListingExample'; + +function SeaportCreateListingDefault({ disabled, handleExampleSubmitted }: RequestExampleProps) { + const { orderbookClient } = useImmutableProvider(); + const { zkEvmProvider } = usePassportProvider(); + + const [params, setParams] = useState(); + + const seaportContractAddress = useMemo( + () => ( + orderbookClient.config().seaportContractAddress + ), + [orderbookClient], + ); + + useEffect(() => { + const getAddress = async () => { + if (zkEvmProvider) { + const [address] = await zkEvmProvider.request({ + method: 'eth_requestAccounts', + }); + const chainIdHex = await zkEvmProvider.request({ method: 'eth_chainId' }); + const chainId = parseInt(chainIdHex, 16); + const data = getCreateListingPayload({ seaportContractAddress, walletAddress: address, chainId }); + setParams([address, data]); + } + }; + getAddress().catch(console.log); + }, [zkEvmProvider, seaportContractAddress]); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + + return handleExampleSubmitted({ + method: 'eth_signTypedData_v4', + params, + }); + }, [handleExampleSubmitted, params]); + + return ( + + Seaport Create Listing (Hardcoded example) + + + Note: This method only returns a signed message, it does not submit an order to the orderbook. + +
+ + + Preview + + + + + + Submit + + +
+
+
+ ); +} + +export default SeaportCreateListingDefault; diff --git a/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingExample.ts b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingExample.ts new file mode 100644 index 0000000000..cd7d517199 --- /dev/null +++ b/packages/passport/sdk-sample-app/src/components/zkevm/EthSendTransactionExamples/SeaportCreateListingExample.ts @@ -0,0 +1,156 @@ +import { OrderType, ItemType } from '@opensea/seaport-js/lib/constants'; + +export const getCreateListingPayload = ({ seaportContractAddress, walletAddress, chainId }: +{ seaportContractAddress: string, walletAddress: string, chainId: number }) => ({ + domain: { + chainId, + name: 'ImmutableSeaport', + verifyingContract: seaportContractAddress, + version: '1.5', + }, + message: { + conduitKey: '0x0000000000000000000000000000000000000000000000000000000000000000', + consideration: [ + { + endAmount: '1000000000000000000', + identifierOrCriteria: '0', + itemType: ItemType.NATIVE, + recipient: walletAddress, + startAmount: '1000000000000000000', + token: '0x0000000000000000000000000000000000000000', + }, + ], + counter: '0', + endTime: '1772076852', + offer: [ + { + endAmount: '1', + identifierOrCriteria: '194', + itemType: ItemType.ERC721, + startAmount: '1', + token: '0x79eb1ef399e35a6eb267cc0eda9e88d8052a8ee7', + }, + ], + offerer: walletAddress, + orderType: OrderType.FULL_RESTRICTED, + salt: '14819419685848783759', + startTime: '1709004852', + zone: '0x8831867e347ab87fa30199c5b695f0a31604bb52', + zoneHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + }, + primaryType: 'OrderComponents', + types: { + ConsiderationItem: [ + { + name: 'itemType', + type: 'uint8', + }, + { + name: 'token', + type: 'address', + }, + { + name: 'identifierOrCriteria', + type: 'uint256', + }, + { + name: 'startAmount', + type: 'uint256', + }, + { + name: 'endAmount', + type: 'uint256', + }, + { + name: 'recipient', + type: 'address', + }, + ], + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'chainId', + type: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + OfferItem: [ + { + name: 'itemType', + type: 'uint8', + }, + { + name: 'token', + type: 'address', + }, + { + name: 'identifierOrCriteria', + type: 'uint256', + }, + { + name: 'startAmount', + type: 'uint256', + }, + { + name: 'endAmount', + type: 'uint256', + }, + ], + OrderComponents: [ + { + name: 'offerer', + type: 'address', + }, + { + name: 'zone', + type: 'address', + }, + { + name: 'offer', + type: 'OfferItem[]', + }, + { + name: 'consideration', + type: 'ConsiderationItem[]', + }, + { + name: 'orderType', + type: 'uint8', + }, + { + name: 'startTime', + type: 'uint256', + }, + { + name: 'endTime', + type: 'uint256', + }, + { + name: 'zoneHash', + type: 'bytes32', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'conduitKey', + type: 'bytes32', + }, + { + name: 'counter', + type: 'uint256', + }, + ], + }, +}); diff --git a/packages/passport/sdk-sample-app/src/components/zkevm/EthSignTypedDataV4Examples/index.ts b/packages/passport/sdk-sample-app/src/components/zkevm/EthSignTypedDataV4Examples/index.ts index 20ed76c58b..503f149f3b 100644 --- a/packages/passport/sdk-sample-app/src/components/zkevm/EthSignTypedDataV4Examples/index.ts +++ b/packages/passport/sdk-sample-app/src/components/zkevm/EthSignTypedDataV4Examples/index.ts @@ -1,3 +1,5 @@ +import SeaportCreateListing from '../EthSendTransactionExamples/SeaportCreateListing'; +import SeaportCreateListingDefault from '../EthSendTransactionExamples/SeaportCreateListingDefault'; import SignEtherMail from './SignEtherMail'; import ValidateEtherMail from './ValidateEtherMail'; import ValidateSignature from './ValidateSignature'; @@ -6,6 +8,8 @@ const EthSignTypedDataV4Examples = [ ValidateSignature, SignEtherMail, ValidateEtherMail, + SeaportCreateListing, + SeaportCreateListingDefault, ]; export default EthSignTypedDataV4Examples;