From 1d9502525de72896440919b5a65503820783889d Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 18:03:47 +0300 Subject: [PATCH 01/74] use waitForTransactionReceipt from publicClient to listen receipt updates --- .../transactionStatusModal/index.tsx | 93 ++++++++++++------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index 62727ca9..95232d79 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -6,10 +6,9 @@ import { } from '@0xsequence/design-system'; import { observer } from '@legendapp/state/react'; import { Close, Content, Overlay, Portal, Root } from '@radix-ui/react-dialog'; -import type { QueryKey } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import type { Hex } from 'viem'; -import { useTransactionReceipt } from 'wagmi'; +import { type QueryKey } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { WaitForTransactionReceiptTimeoutError, type Hex } from 'viem'; import type { Price } from '../../../../../../types'; import type { BaseCallbacks } from '../../../../../../types/callbacks'; import { getQueryClient } from '../../../../../_internal'; @@ -27,6 +26,8 @@ import { transactionStatusModalContent, } from './styles.css'; import { ChainId } from '@0xsequence/network'; +import { getPublicRpcClient } from '../../../../../../utils'; +import { TRANSACTION_CONFIRMATIONS_DEFAULT } from '@0xsequence/kit'; export type ShowTransactionStatusModalArgs = { hash: Hex; @@ -66,40 +67,62 @@ const TransactionStatusModal = observer(() => { chainId, collectibleId: tokenId, }); - const { - data: transaction, - isLoading: isConfirming, - isSuccess: isConfirmed, - isError: isFailed, - error, - } = useTransactionReceipt({ hash }); - const title = getTitle && getTitle({ isConfirmed, isConfirming, isFailed }); + const [transactionStatus, setTransactionStatus] = useState< + 'pending' | 'success' | 'error' | 'timeout' + >('pending'); + const title = + getTitle && + getTitle({ + isConfirmed: transactionStatus === 'success', + isConfirming: transactionStatus === 'pending', + isFailed: transactionStatus === 'error', + isTimeout: transactionStatus === 'timeout', + }); const message = - getMessage && getMessage({ isConfirmed, isConfirming, isFailed }); + getMessage && + getMessage({ + isConfirmed: transactionStatus === 'success', + isConfirming: transactionStatus === 'pending', + isFailed: transactionStatus === 'error', + isTimeout: transactionStatus === 'timeout', + }); const { onUnknownError, onSuccess }: BaseCallbacks = callbacks || {}; const queryClient = getQueryClient(); + const publicClient = chainId ? getPublicRpcClient(chainId) : null; + const waitForTransactionReceiptPromise = + publicClient?.waitForTransactionReceipt({ + confirmations: TRANSACTION_CONFIRMATIONS_DEFAULT, + hash: hash!, + }); useEffect(() => { if (!transactionStatusModal$.isOpen.get()) return; - let isSubscribed = true; - - if (isConfirmed && isSubscribed && onSuccess) { - onSuccess(); - } - - if (isFailed && isSubscribed && onUnknownError) { - onUnknownError(error); - } - - if (isSubscribed && queriesToInvalidate) { + console.log('Waiting for transaction receipt ...'); + waitForTransactionReceiptPromise + ?.then((receipt) => { + if (receipt.status === 'success') { + console.log('receipt', receipt); + setTransactionStatus('success'); + } + }) + .catch((error) => { + if (error instanceof WaitForTransactionReceiptTimeoutError) { + setTransactionStatus('timeout'); + return; + } + + setTransactionStatus('error'); + }); + + if (queriesToInvalidate) { queryClient.invalidateQueries({ queryKey: [...queriesToInvalidate] }); } - return () => { - isSubscribed = false; - }; - }, [isConfirmed, isFailed, onSuccess, onUnknownError, error]); + return () => { + setTransactionStatus('pending'); + } + }, [onSuccess, onUnknownError, transactionStatusModal$.isOpen.get()]); return ( @@ -135,17 +158,19 @@ const TransactionStatusModal = observer(() => { chainId={chainId} collectible={collectible} currencyImageUrl={price?.currency.imageUrl} - isConfirming={isConfirming} - isConfirmed={isConfirmed} - isFailed={isFailed || transaction?.status === 'reverted'} + isConfirming={transactionStatus === 'pending'} + isConfirmed={transactionStatus === 'success'} + isFailed={transactionStatus === 'error'} + isTimeout={transactionStatus === 'timeout'} /> )} From 0945fdff0dfd1b7e3ac00c1ab728b74601f8a51e Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 18:04:26 +0300 Subject: [PATCH 02/74] add isTimout prop to components --- .../modals/_internal/components/transaction-footer/index.tsx | 5 ++++- .../modals/_internal/components/transactionPreview/index.tsx | 4 +++- .../_internal/components/transactionStatusModal/store.ts | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transaction-footer/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transaction-footer/index.tsx index ab914fb8..77564323 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transaction-footer/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transaction-footer/index.tsx @@ -9,6 +9,7 @@ type TransactionFooterProps = { isConfirming: boolean; isConfirmed: boolean; isFailed: boolean; + isTimeout: boolean; chainId: ChainId; }; @@ -17,6 +18,7 @@ export default function TransactionFooter({ isConfirming, isConfirmed, isFailed, + isTimeout, chainId, }: TransactionFooterProps) { const icon = @@ -26,7 +28,8 @@ export default function TransactionFooter({ const title = (isConfirming && 'Processing transaction') || (isConfirmed && 'Transaction complete') || - (isFailed && 'Transaction failed'); + (isFailed && 'Transaction failed') || + (isTimeout && 'Transaction took longer than expected'); const transactionUrl = `${networks[chainId as unknown as ChainId]?.blockExplorer?.rootUrl}tx/${transactionHash}`; return ( diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx index 852d7196..39990971 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx @@ -18,6 +18,7 @@ type TransactionPreviewProps = { isConfirming: boolean; isConfirmed: boolean; isFailed: boolean; + isTimeout: boolean; }; const TransactionPreview = observer( @@ -30,10 +31,11 @@ const TransactionPreview = observer( isConfirming, isConfirmed, isFailed, + isTimeout }: TransactionPreviewProps) => { const { type } = transactionStatusModal$.state.get(); const title = useTransactionPreviewTitle( - { isConfirmed, isConfirming, isFailed }, + { isConfirmed, isConfirming, isFailed, isTimeout }, type, ); const { data: collection } = useCollection({ diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index b9091997..bf592bd7 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -11,6 +11,7 @@ export type ConfirmationStatus = { isConfirming: boolean; isConfirmed: boolean; isFailed: boolean; + isTimeout: boolean; }; export type TransactionStatusExtended = TransactionStatus | 'PENDING'; From fcfcfa74ce1c23ada419dee472e0455a3e18236e Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 18:05:00 +0300 Subject: [PATCH 03/74] add new messages for timed out transaction receipt fetchs --- .../_utils/getCreateListingTransactionTitleMessage.ts | 8 ++++++++ .../_utils/getMakeOfferTransactionTitleMessage.ts | 8 ++++++++ .../SellModal/_utils/getSellTransactionTitleMessage.ts | 9 +++++++++ .../_utils/getTransferTransactionTitleMessage.ts | 9 +++++++++ 4 files changed, 34 insertions(+) diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts index 76824ab1..f15655ec 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts @@ -11,6 +11,10 @@ export const getCreateListingTransactionTitle = ( return 'Listing has failed'; } + if (params.isTimeout) { + return 'Your listing is taking longer than expected'; + } + return 'Listing is processing'; }; @@ -26,5 +30,9 @@ export const getCreateListingTransactionMessage = ( return `Your listing of ${collectibleName} has failed. Please try again.`; } + if (params.isTimeout) { + return `Your listing is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; + } + return `You just listed ${collectibleName}. It should be confirmed on the blockchain shortly.`; }; diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts index 4af83a08..022e30cf 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts @@ -9,6 +9,10 @@ export const getMakeOfferTransactionTitle = (params: ConfirmationStatus) => { return 'Your offer has failed'; } + if (params.isTimeout) { + return 'Your offer is taking longer than expected'; + } + return 'Your offer is processing'; }; @@ -24,5 +28,9 @@ export const getMakeOfferTransactionMessage = ( return `Your offer for ${collectibleName} has failed. Please try again.`; } + if (params.isTimeout) { + return `Your offer is still being processed. This may take a little longer than usual. Please continue with transaction hash below to check the status on explorer.`; + } + return `You just made offer for ${collectibleName}. It should be confirmed on the blockchain shortly.`; }; diff --git a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts index f61e31fe..f6561c41 100644 --- a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts @@ -9,6 +9,11 @@ export const getSellTransactionTitle = (params: ConfirmationStatus) => { return 'Your sale has failed'; } + + if (params.isTimeout) { + return 'Your sale is taking longer than expected'; + } + return 'Your sale is processing'; }; @@ -24,5 +29,9 @@ export const getSellTransactionMessage = ( return `Your sale of ${collectibleName} has failed. Please try again.`; } + if (params.isTimeout) { + return `Your sale is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; + } + return `You just sold ${collectibleName}. It should be confirmed on the blockchain shortly.`; }; diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts index 9d9c080f..73f01586 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts @@ -9,6 +9,11 @@ export const getTransferTransactionTitle = (params: ConfirmationStatus) => { return 'Transfer has failed'; } + if (params.isTimeout) { + return 'Transfer is taking longer than expected'; + } + + return 'Transfer is processing'; }; @@ -24,5 +29,9 @@ export const getTransferTransactionMessage = ( return `Transferring ${collectibleName} has failed. Please try again.`; } + if (params.isTimeout) { + return `Transfer is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; + } + return `You just transferred ${collectibleName}. It should be confirmed on the blockchain shortly.`; }; From d17ebe9e77f565a3f672dc029b6d397ea8d2f3d4 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 18:47:56 +0300 Subject: [PATCH 04/74] remove useTransactionStatusModal from modals --- .../ui/modals/CreateListingModal/index.tsx | 49 ++--------------- .../react/ui/modals/MakeOfferModal/index.tsx | 50 +++-------------- .../src/react/ui/modals/SellModal/index.tsx | 49 ++--------------- .../enterWalletAddress/useHandleTransfer.tsx | 53 ++----------------- 4 files changed, 18 insertions(+), 183 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx index c088844b..99b54800 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx @@ -1,13 +1,10 @@ import { Box } from '@0xsequence/design-system'; import { Show, observer } from '@legendapp/state/react'; -import type { QueryKey } from '@tanstack/react-query'; import type { Hash, Hex } from 'viem'; import { type ContractType, - StepType, - collectableKeys, } from '../../../_internal'; -import { useCollectible, useCollection } from '../../../hooks'; +import { useCollection } from '../../../hooks'; import { useCreateListing } from '../../../hooks/useCreateListing'; import { ActionModal, @@ -21,13 +18,8 @@ import PriceInput from '../_internal/components/priceInput'; import QuantityInput from '../_internal/components/quantityInput'; import TokenPreview from '../_internal/components/tokenPreview'; import TransactionDetails from '../_internal/components/transactionDetails'; -import { useTransactionStatusModal } from '../_internal/components/transactionStatusModal'; import type { ModalCallbacks } from '../_internal/types'; import { createListingModal$ } from './_store'; -import { - getCreateListingTransactionMessage, - getCreateListingTransactionTitle, -} from './_utils/getCreateListingTransactionTitleMessage'; export type ShowCreateListingModalArgs = { collectionAddress: Hex; @@ -46,35 +38,16 @@ export const useCreateListingModal = (callbacks?: ModalCallbacks) => { }; export const CreateListingModal = () => { - const { show: showTransactionStatusModal } = useTransactionStatusModal(); return ( - + ); }; -type TransactionStatusModalReturn = ReturnType< - typeof useTransactionStatusModal ->; - -export const Modal = observer( - ({ - showTransactionStatusModal, - }: { - showTransactionStatusModal: TransactionStatusModalReturn['show']; - }) => { +export const Modal = observer(() => { const state = createListingModal$.get(); const { collectionAddress, chainId, listingPrice, collectibleId } = state; - const { - data: collectible, - isLoading: collectableIsLoading, - isError: collectableIsError, - } = useCollectible({ - chainId, - collectionAddress, - collectibleId, - }); const { data: collection, isLoading: collectionIsLoading, @@ -89,18 +62,6 @@ export const Modal = observer( collectionAddress, onTransactionSent: (hash) => { if (!hash) return; - showTransactionStatusModal({ - hash, - collectionAddress, - chainId, - price: createListingModal$.listingPrice.get(), - tokenId: collectibleId, - getTitle: getCreateListingTransactionTitle, - getMessage: (params) => - getCreateListingTransactionMessage(params, collectible?.name || ''), - type: StepType.createListing, - queriesToInvalidate: collectableKeys.all as unknown as QueryKey[], - }); createListingModal$.close(); }, onError: (error) => { @@ -123,7 +84,7 @@ export const Modal = observer( } }; - if (collectableIsLoading || collectionIsLoading) { + if (collectionIsLoading) { return ( ({ }); export const MakeOfferModal = () => { - const { show: showTransactionStatusModal } = useTransactionStatusModal(); return ( - + ); }; -type TransactionStatusModalReturn = ReturnType< - typeof useTransactionStatusModal ->; - const ModalContent = observer( - ({ - showTransactionStatusModal, - }: { - showTransactionStatusModal: TransactionStatusModalReturn['show']; - }) => { + () => { const state = makeOfferModal$.get(); const { collectionAddress, chainId, offerPrice, collectibleId } = state; const [insufficientBalance, setInsufficientBalance] = useState(false); - const { - data: collectible, - isLoading: collectableIsLoading, - isError: collectableIsError, - } = useCollectible({ - chainId, - collectionAddress, - collectibleId, - }); - const { data: collection, isLoading: collectionIsLoading, @@ -85,18 +60,7 @@ const ModalContent = observer( collectionAddress, onTransactionSent: (hash) => { if (!hash) return; - showTransactionStatusModal({ - hash, - price: makeOfferModal$.offerPrice.get(), - collectionAddress, - chainId, - tokenId: collectibleId, - getTitle: getMakeOfferTransactionTitle, - getMessage: (params) => - getMakeOfferTransactionMessage(params, collectible?.name || ''), - type: StepType.createOffer, - queriesToInvalidate: collectableKeys.all as unknown as QueryKey[], - }); + makeOfferModal$.close(); }, onSuccess: (hash) => { @@ -136,7 +100,7 @@ const ModalContent = observer( refreshSteps(); }, [currencyAddress]); - if (collectableIsLoading || collectionIsLoading || currenciesIsLoading) { + if (collectionIsLoading || currenciesIsLoading) { return ( ({ }); export const SellModal = () => { - const { show: showTransactionStatusModal } = useTransactionStatusModal(); return ( - + ); }; -type TransactionStatusModalReturn = ReturnType< - typeof useTransactionStatusModal ->; - const ModalContent = observer( - ({ - showTransactionStatusModal, - }: { - showTransactionStatusModal: TransactionStatusModalReturn['show']; - }) => { + () => { const { tokenId, collectionAddress, chainId, order } = sellModal$.get(); - const { data: collectible } = useCollection({ - chainId, - collectionAddress, - }); - const { sell } = useSell({ collectionAddress, chainId, onTransactionSent: (hash) => { if (!hash) return; - showTransactionStatusModal({ - hash: hash, - price: { - amountRaw: order!.priceAmount, - currency: currencies!.find( - (currency) => - currency.contractAddress === order!.priceCurrencyAddress, - )!, - }, - collectionAddress, - chainId, - tokenId, - getTitle: getSellTransactionTitle, - getMessage: (params) => - getSellTransactionMessage(params, collectible?.name || ''), - type: StepType.sell, - queriesToInvalidate: [ - ...collectableKeys.all, - balanceQueries.all, - ] as unknown as QueryKey[], - }); + sellModal$.close(); }, onSuccess: (hash) => { diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx index 6306420b..fba804fe 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx @@ -1,14 +1,7 @@ -import type { QueryKey } from '@tanstack/react-query'; import type { Hex } from 'viem'; import { ContractType } from '../../../../../../types'; -import { balanceQueries } from '../../../../../_internal'; -import { useCollectible, useTransferTokens } from '../../../../../hooks'; -import { useTransactionStatusModal } from '../../../_internal/components/transactionStatusModal'; +import { useTransferTokens } from '../../../../../hooks'; import { transferModal$ } from '../../_store'; -import { - getTransferTransactionMessage, - getTransferTransactionTitle, -} from '../../_utils/getTransferTransactionTitleMessage'; const useHandleTransfer = () => { const { @@ -18,16 +11,9 @@ const useHandleTransfer = () => { quantity, chainId, collectionType, - successCallbacks, errorCallbacks, } = transferModal$.state.get(); const { transferTokensAsync } = useTransferTokens(); - const { show: showTransactionStatusModal } = useTransactionStatusModal(); - const { data: collectible } = useCollectible({ - collectionAddress, - collectibleId: tokenId, - chainId, - }); async function transfer() { if ( @@ -39,7 +25,7 @@ const useHandleTransfer = () => { if (collectionType === ContractType.ERC721) { try { - const hash = await transferTokensAsync({ + await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, tokenId, @@ -48,23 +34,6 @@ const useHandleTransfer = () => { }); transferModal$.close(); - - showTransactionStatusModal({ - hash: hash, - collectionAddress, - chainId, - tokenId, - price: undefined, - getTitle: getTransferTransactionTitle, - getMessage: (params) => - getTransferTransactionMessage(params, collectible!.name), - type: 'transfer', - callbacks: { - onSuccess: successCallbacks?.onTransferSuccess, - onUnknownError: errorCallbacks?.onTransferError, - }, - queriesToInvalidate: balanceQueries.all as unknown as QueryKey[], - }); } catch (error) { transferModal$.view.set('enterReceiverAddress'); @@ -74,7 +43,7 @@ const useHandleTransfer = () => { if (collectionType === ContractType.ERC1155) { try { - const hash = await transferTokensAsync({ + await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, tokenId, @@ -84,22 +53,6 @@ const useHandleTransfer = () => { }); transferModal$.close(); - - showTransactionStatusModal({ - hash: hash, - collectionAddress, - chainId, - tokenId, - price: undefined, - getTitle: getTransferTransactionTitle, - getMessage: (params) => - getTransferTransactionMessage(params, collectible!.name), - type: 'transfer', - callbacks: { - onSuccess: successCallbacks?.onTransferSuccess, - onUnknownError: errorCallbacks?.onTransferError, - }, - }); } catch (error) { transferModal$.view.set('enterReceiverAddress'); From dba7c4173415ded9f71ee745bdb27ba87c794361 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 19:15:03 +0300 Subject: [PATCH 05/74] add TRANSFER to TransactionType --- .../transaction-machine/execute-transaction.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index bcd7377c..782a6935 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -26,6 +26,7 @@ import { type Step, StepType, } from '../../../types'; +import { useTransactionStatusModal } from '../../ui/modals/_internal/components/transactionStatusModal'; export enum TransactionState { IDLE = 'IDLE', @@ -42,6 +43,7 @@ export enum TransactionType { BUY = 'BUY', SELL = 'SELL', LISTING = 'LISTING', + TRANSFER = 'TRANSFER', OFFER = 'OFFER', CANCEL = 'CANCEL', } @@ -52,6 +54,7 @@ export interface TransactionConfig { chainId: string; chains: readonly Chain[]; collectionAddress: string; + collectibleId?: string; sdkConfig: SdkConfig; marketplaceConfig: MarketplaceConfig; } @@ -384,6 +387,15 @@ export class TransactionMachine { }; debug('Executing transaction', transactionData); const hash = await this.walletClient.sendTransaction(transactionData); + + useTransactionStatusModal().show({ + chainId: this.getChainId()! as unknown as string, + collectionAddress: this.config.config.collectionAddress as Hex, + collectibleId: this.config.config.collectibleId as string, + hash: hash as Hash, + type: this.config.config.type, + }) + debug('Transaction submitted', { hash }); await this.handleTransactionSuccess(hash); return hash; From 3045b2b4b55699eab539ecedc504b63f934238ac Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 19:54:04 +0300 Subject: [PATCH 06/74] add cancel titles --- .../components/transactionPreview/consts.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/consts.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/consts.ts index b8c0f7a5..a29daacc 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/consts.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/consts.ts @@ -1,27 +1,32 @@ export const TRANSACTION_TITLES = { - sell: { + SELL: { confirming: 'Selling', confirmed: 'Sold', failed: 'Sale failed', }, - createListing: { + LISTING: { confirming: 'Creating listing', confirmed: 'Listed', failed: 'Listing failed', }, - createOffer: { + OFFER: { confirming: 'Creating offer', confirmed: 'Offer created', failed: 'Offer failed', }, - buy: { + BUY: { confirming: 'Buying', confirmed: 'Bought', failed: 'Purchase failed', }, - transfer: { + TRANSFER: { confirming: 'Transferring', confirmed: 'Transferred', failed: 'Transfer failed', }, + CANCEL: { + confirming: 'Cancelling', + confirmed: 'Cancelled', + failed: 'Cancellation failed', + }, } as const; From d9071f18965c29426de292d840481826ba3cecdf Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 19:55:01 +0300 Subject: [PATCH 07/74] update modal callbacks --- .../_internals/action-button/ActionButton.tsx | 4 +- .../src/react/ui/modals/BuyModal/index.tsx | 3 +- .../react/ui/modals/TransferModal/_store.ts | 17 +++---- .../react/ui/modals/TransferModal/index.tsx | 16 +++--- .../useTransactionPreviewTitle.tsx | 4 +- .../transactionStatusModal/index.tsx | 18 +++---- .../transactionStatusModal/store.ts | 27 ++++------ packages/sdk/src/types/callbacks.ts | 51 ------------------- 8 files changed, 38 insertions(+), 102 deletions(-) delete mode 100644 packages/sdk/src/types/callbacks.ts diff --git a/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx b/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx index fb128a65..8e44f5c7 100644 --- a/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx +++ b/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx @@ -55,7 +55,7 @@ export const ActionButton = observer( showBuyModal({ collectionAddress, chainId: chainId, - tokenId: tokenId, + collectibleId: tokenId, order: lowestListing, }) } @@ -120,7 +120,7 @@ export const ActionButton = observer( showTransferModal({ collectionAddress: collectionAddress as Hex, chainId: chainId, - tokenId, + collectibleId:tokenId, }) } /> diff --git a/packages/sdk/src/react/ui/modals/BuyModal/index.tsx b/packages/sdk/src/react/ui/modals/BuyModal/index.tsx index 6657d85c..726d3787 100644 --- a/packages/sdk/src/react/ui/modals/BuyModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/BuyModal/index.tsx @@ -13,7 +13,7 @@ import { TokenMetadata } from '@0xsequence/indexer'; export type ShowBuyModalArgs = { chainId: string; collectionAddress: Hex; - tokenId: string; + collectibleId: string; order: Order; }; @@ -44,6 +44,7 @@ export const BuyModalContent = () => { const { buy } = useBuyCollectable({ chainId, collectionAddress, + collectibleId, }); const { data: collectable } = useCollectible({ diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_store.ts b/packages/sdk/src/react/ui/modals/TransferModal/_store.ts index 1371e255..5181a3b8 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_store.ts +++ b/packages/sdk/src/react/ui/modals/TransferModal/_store.ts @@ -1,11 +1,8 @@ import { observable } from '@legendapp/state'; import type { Hex } from 'viem'; import type { ShowTransferModalArgs } from '.'; -import type { - TransferErrorCallbacks, - TransferSuccessCallbacks, -} from '../../../../types/callbacks'; import type { CollectionType } from '../../../_internal'; +import { ModalCallbacks } from '../_internal/types'; export interface TransferModalState { isOpen: boolean; @@ -15,11 +12,10 @@ export interface TransferModalState { chainId: string; collectionAddress: Hex; collectionType?: CollectionType | undefined; - tokenId: string; + collectibleId: string; quantity: string; receiverAddress: string; - errorCallbacks?: TransferErrorCallbacks; - successCallbacks?: TransferSuccessCallbacks; + callbacks?: ModalCallbacks; }; view: 'enterReceiverAddress' | 'followWalletInstructions' | undefined; hash: Hex | undefined; @@ -27,12 +23,13 @@ export interface TransferModalState { export const initialState: TransferModalState = { isOpen: false, - open: ({ chainId, collectionAddress, tokenId }: ShowTransferModalArgs) => { + open: ({ chainId, collectionAddress, collectibleId, callbacks }: ShowTransferModalArgs) => { transferModal$.state.set({ ...transferModal$.state.get(), chainId, collectionAddress, - tokenId, + collectibleId, + callbacks }); transferModal$.isOpen.set(true); }, @@ -48,7 +45,7 @@ export const initialState: TransferModalState = { receiverAddress: '', collectionAddress: '0x', chainId: '', - tokenId: '', + collectibleId: '', quantity: '1', }, view: 'enterReceiverAddress', diff --git a/packages/sdk/src/react/ui/modals/TransferModal/index.tsx b/packages/sdk/src/react/ui/modals/TransferModal/index.tsx index a4a07ac5..f68d5b43 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/index.tsx @@ -3,20 +3,18 @@ import { Show, observer } from '@legendapp/state/react'; import { Close, Content, Overlay, Portal, Root } from '@radix-ui/react-dialog'; import type { Hex } from 'viem'; import { useAccount } from 'wagmi'; -import type { - TransferErrorCallbacks, - TransferSuccessCallbacks, -} from '../../../../types/callbacks'; import { useSwitchChainModal } from '../_internal/components/switchChainModal'; import { transferModal$ } from './_store'; import EnterWalletAddressView from './_views/enterWalletAddress'; import FollowWalletInstructionsView from './_views/followWalletInstructions'; import { closeButton, dialogOverlay, transferModalContent } from './styles.css'; +import { ModalCallbacks } from '../_internal/types'; export type ShowTransferModalArgs = { collectionAddress: Hex; - tokenId: string; + collectibleId: string; chainId: string; + callbacks?: ModalCallbacks; }; export const useTransferModal = () => { @@ -45,16 +43,16 @@ export const useTransferModal = () => { return { show: handleShowModal, close: () => transferModal$.close(), - onError: (callbacks: TransferErrorCallbacks) => { + onError: (callbacks: ModalCallbacks) => { transferModal$.state.set({ ...transferModal$.state.get(), - errorCallbacks: callbacks, + callbacks }); }, - onSuccess: (callbacks: TransferSuccessCallbacks) => { + onSuccess: (callbacks: ModalCallbacks) => { transferModal$.state.set({ ...transferModal$.state.get(), - successCallbacks: callbacks, + callbacks }); }, }; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx index 21baf242..73b1b600 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx @@ -1,13 +1,13 @@ import { useMemo } from 'react'; import type { ConfirmationStatus, - StatusOrderType, } from '../transactionStatusModal/store'; import { TRANSACTION_TITLES } from './consts'; +import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; export function useTransactionPreviewTitle( status: ConfirmationStatus, - type?: StatusOrderType | undefined, + type?: TransactionType | undefined, ): string { return useMemo(() => { if (!type) return ''; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index 95232d79..62f82317 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -10,14 +10,12 @@ import { type QueryKey } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { WaitForTransactionReceiptTimeoutError, type Hex } from 'viem'; import type { Price } from '../../../../../../types'; -import type { BaseCallbacks } from '../../../../../../types/callbacks'; import { getQueryClient } from '../../../../../_internal'; import { useCollectible } from '../../../../../hooks'; import TransactionFooter from '../transaction-footer'; import TransactionPreview from '../transactionPreview'; import { type ConfirmationStatus, - type StatusOrderType, transactionStatusModal$, } from './store'; import { @@ -28,17 +26,19 @@ import { import { ChainId } from '@0xsequence/network'; import { getPublicRpcClient } from '../../../../../../utils'; import { TRANSACTION_CONFIRMATIONS_DEFAULT } from '@0xsequence/kit'; +import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; +import { ModalCallbacks } from '../../types'; export type ShowTransactionStatusModalArgs = { hash: Hex; price?: Price; collectionAddress: Hex; chainId: string; - tokenId: string; + collectibleId: string; getTitle?: (props: ConfirmationStatus) => string; getMessage?: (props: ConfirmationStatus) => string; - type: StatusOrderType; - callbacks?: BaseCallbacks; + type: TransactionType; + callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; }; @@ -56,7 +56,7 @@ const TransactionStatusModal = observer(() => { price, collectionAddress, chainId, - tokenId, + collectibleId, getTitle, getMessage, callbacks, @@ -65,7 +65,7 @@ const TransactionStatusModal = observer(() => { const { data: collectible } = useCollectible({ collectionAddress, chainId, - collectibleId: tokenId, + collectibleId, }); const [transactionStatus, setTransactionStatus] = useState< 'pending' | 'success' | 'error' | 'timeout' @@ -86,7 +86,7 @@ const TransactionStatusModal = observer(() => { isFailed: transactionStatus === 'error', isTimeout: transactionStatus === 'timeout', }); - const { onUnknownError, onSuccess }: BaseCallbacks = callbacks || {}; + const { onError, onSuccess }: ModalCallbacks = callbacks || {}; const queryClient = getQueryClient(); const publicClient = chainId ? getPublicRpcClient(chainId) : null; const waitForTransactionReceiptPromise = @@ -122,7 +122,7 @@ const TransactionStatusModal = observer(() => { return () => { setTransactionStatus('pending'); } - }, [onSuccess, onUnknownError, transactionStatusModal$.isOpen.get()]); + }, [onSuccess, onError, transactionStatusModal$.isOpen.get()]); return ( diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index bf592bd7..2b69281d 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -4,8 +4,8 @@ import type { QueryKey } from '@tanstack/react-query'; import type { Hex } from 'viem'; import type { ShowTransactionStatusModalArgs } from '.'; import type { Price } from '../../../../../../types'; -import type { BaseCallbacks } from '../../../../../../types/callbacks'; -import type { StepType } from '../../../../../_internal'; +import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; +import { ModalCallbacks } from '../../types'; export type ConfirmationStatus = { isConfirming: boolean; @@ -16,16 +16,6 @@ export type ConfirmationStatus = { export type TransactionStatusExtended = TransactionStatus | 'PENDING'; -export type StatusOrderType = - | Pick< - typeof StepType, - 'sell' | 'createListing' | 'createOffer' | 'buy' - >[keyof Pick< - typeof StepType, - 'sell' | 'createListing' | 'createOffer' | 'buy' - >] - | 'transfer'; - export interface TransactionStatusModalState { isOpen: boolean; open: (args: ShowTransactionStatusModalArgs) => void; @@ -33,14 +23,14 @@ export interface TransactionStatusModalState { state: { hash: Hex | undefined; status: TransactionStatusExtended; - type: StatusOrderType | undefined; + type: TransactionType | undefined; price: Price | undefined; collectionAddress: Hex; chainId: string; - tokenId: string; + collectibleId: string; getTitle?: (params: ConfirmationStatus) => string; getMessage?: (params: ConfirmationStatus) => string; - callbacks?: BaseCallbacks; + callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; }; } @@ -52,7 +42,7 @@ export const initialState: TransactionStatusModalState = { price, collectionAddress, chainId, - tokenId, + collectibleId, getTitle, getMessage, type, @@ -65,7 +55,7 @@ export const initialState: TransactionStatusModalState = { price, collectionAddress, chainId, - tokenId, + collectibleId, getTitle, getMessage, type, @@ -86,10 +76,11 @@ export const initialState: TransactionStatusModalState = { price: undefined, collectionAddress: '' as Hex, chainId: '', - tokenId: '', + collectibleId: '', getTitle: undefined, getMessage: undefined, type: undefined, + callbacks: undefined, }, }; diff --git a/packages/sdk/src/types/callbacks.ts b/packages/sdk/src/types/callbacks.ts deleted file mode 100644 index 8421f5f4..00000000 --- a/packages/sdk/src/types/callbacks.ts +++ /dev/null @@ -1,51 +0,0 @@ -export type BaseCallbacks = { - onSuccess?: () => void; - onUnknownError?: (error: Error | unknown) => void; -}; - -export type SwitchChainCallbacks = BaseCallbacks & { - onSwitchingNotSupported?: () => void; - onUserRejectedRequest?: () => void; -}; - -export type OnApproveTokenError = (error: Error | unknown) => void; - -export type BaseErrorCallbacks = { - onApproveTokenError?: OnApproveTokenError; - onSwitchingNotSupportedError?: () => void; - onUserRejectedSwitchingChainRequestError?: () => void; - onSwitchChainError?: (error: Error | unknown) => void; -}; - -export type BaseSuccessCallbacks = { - onApproveTokenSuccess?: () => void; - onSwitchChainSuccess?: () => void; -}; - -export type CreateListingErrorCallbacks = BaseErrorCallbacks & { - onCreateListingError?: (error: Error | unknown) => void; -}; -export type CreateListingSuccessCallbacks = BaseSuccessCallbacks & { - onCreateListingSuccess?: () => void; -}; - -export type MakeOfferErrorCallbacks = BaseErrorCallbacks & { - onMakeOfferError?: (error: Error | unknown) => void; -}; -export type MakeOfferSuccessCallbacks = BaseSuccessCallbacks & { - onMakeOfferSuccess?: () => void; -}; - -export type SellErrorCallbacks = BaseErrorCallbacks & { - onSellError?: (error: Error | unknown) => void; -}; -export type SellSuccessCallbacks = BaseSuccessCallbacks & { - onSellSuccess?: () => void; -}; - -export type TransferErrorCallbacks = BaseErrorCallbacks & { - onTransferError?: (error: Error | unknown) => void; -}; -export type TransferSuccessCallbacks = BaseSuccessCallbacks & { - onTransferSuccess?: () => void; -}; From 2cdf4c1095e4097caee543ba2ababf947674b195 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Thu, 5 Dec 2024 19:55:48 +0300 Subject: [PATCH 08/74] return hash from transfer transaction and show transaction status modal when it's done --- .../_views/enterWalletAddress/index.tsx | 27 +++++++++++++++---- .../enterWalletAddress/useHandleTransfer.tsx | 18 +++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx index 05b9253f..13f27b2b 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx @@ -8,10 +8,13 @@ import getMessage from '../../messages'; import useHandleTransfer from './useHandleTransfer'; import { useCollection, useListBalances } from '../../../../..'; import { type CollectionType, ContractType } from '../../../../../_internal'; +import { useTransactionStatusModal } from '../../../_internal/components/transactionStatusModal'; +import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; +import { ModalCallbacks } from '../../../_internal/types'; const EnterWalletAddressView = () => { const { address } = useAccount(); - const { collectionAddress, tokenId, chainId, collectionType } = + const { collectionAddress, collectibleId, chainId, collectionType, callbacks } = transferModal$.state.get(); const $quantity = transferModal$.state.quantity; const isWalletAddressValid = isAddress( @@ -20,7 +23,7 @@ const EnterWalletAddressView = () => { const { data: tokenBalance } = useListBalances({ chainId, contractAddress: collectionAddress, - tokenId, + tokenId: collectibleId, accountAddress: address!, query: { enabled: !!address }, }); @@ -34,6 +37,7 @@ const EnterWalletAddressView = () => { collection?.type as CollectionType | undefined, ); const { transfer } = useHandleTransfer(); + const {show: showTransactionStatusModal} = useTransactionStatusModal(); function handleChangeWalletAddress( event: React.ChangeEvent, @@ -41,8 +45,21 @@ const EnterWalletAddressView = () => { transferModal$.state.receiverAddress.set(event.target.value); } - function handleChangeView() { - transfer(); + async function handleChangeView() { + transfer().then((hash) => { + + showTransactionStatusModal({ + collectionAddress, + collectibleId, + chainId, + hash: hash!, + callbacks: callbacks as ModalCallbacks, + type:TransactionType.TRANSFER, + }); + } + ).catch(() => { + transferModal$.view.set('enterReceiverAddress'); + }) transferModal$.view.set('followWalletInstructions'); } @@ -73,7 +90,7 @@ const EnterWalletAddressView = () => { $quantity={$quantity} chainId={chainId} collectionAddress={collectionAddress} - collectibleId={tokenId} + collectibleId={collectibleId} /> { const { receiverAddress, collectionAddress, - tokenId, + collectibleId, quantity, chainId, collectionType, - errorCallbacks, + callbacks } = transferModal$.state.get(); const { transferTokensAsync } = useTransferTokens(); @@ -25,38 +25,40 @@ const useHandleTransfer = () => { if (collectionType === ContractType.ERC721) { try { - await transferTokensAsync({ + const hash = await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, - tokenId, + tokenId: collectibleId, chainId, contractType: ContractType.ERC721, }); transferModal$.close(); + return hash; } catch (error) { transferModal$.view.set('enterReceiverAddress'); - errorCallbacks?.onTransferError?.(error); + callbacks?.onError?.(error as Error); } } if (collectionType === ContractType.ERC1155) { try { - await transferTokensAsync({ + const hash = await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, - tokenId, + tokenId: collectibleId, chainId, contractType: ContractType.ERC1155, quantity: String(quantity), }); transferModal$.close(); + return hash; } catch (error) { transferModal$.view.set('enterReceiverAddress'); - errorCallbacks?.onTransferError?.(error); + callbacks?.onError?.(error as Error); } } } From 4e0e41016c9b56d11c0be812f24af4d3117fc580 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:40:00 +0300 Subject: [PATCH 09/74] assign collectibleId and format --- .../execute-transaction.ts | 7 +- .../_internals/action-button/ActionButton.tsx | 2 +- .../src/react/ui/modals/BuyModal/_store.ts | 2 +- .../src/react/ui/modals/BuyModal/index.tsx | 82 ++--- .../ui/modals/CreateListingModal/index.tsx | 260 ++++++++------- .../react/ui/modals/MakeOfferModal/index.tsx | 295 +++++++++--------- .../_utils/getSellTransactionTitleMessage.ts | 1 - .../src/react/ui/modals/SellModal/index.tsx | 195 ++++++------ .../react/ui/modals/TransferModal/_store.ts | 9 +- .../getTransferTransactionTitleMessage.ts | 1 - .../_views/enterWalletAddress/index.tsx | 39 ++- .../enterWalletAddress/useHandleTransfer.tsx | 2 +- .../react/ui/modals/TransferModal/index.tsx | 4 +- .../components/alertMessage/index.tsx | 7 +- .../react-vite/src/tabs/Collectable.tsx | 5 +- 15 files changed, 463 insertions(+), 448 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 782a6935..8b838598 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -378,6 +378,7 @@ export class TransactionMachine { } private async executeTransaction(step: Step): Promise { + console.log('executeTransaction', step); const transactionData = { account: this.getAccount(), chain: this.getChainForTransaction(), @@ -388,13 +389,15 @@ export class TransactionMachine { debug('Executing transaction', transactionData); const hash = await this.walletClient.sendTransaction(transactionData); + console.log('hash', hash, this.config.config.collectibleId); + useTransactionStatusModal().show({ chainId: this.getChainId()! as unknown as string, collectionAddress: this.config.config.collectionAddress as Hex, collectibleId: this.config.config.collectibleId as string, hash: hash as Hash, type: this.config.config.type, - }) + }); debug('Transaction submitted', { hash }); await this.handleTransactionSuccess(hash); @@ -543,7 +546,7 @@ export class TransactionMachine { } else if (step.id === StepType.tokenApproval) { //TODO: Add some sort ofs callback heres const hash = await this.executeTransaction(step); - return { hash } + return { hash }; } else { const hash = await this.executeTransaction(step); this.config.onSuccess?.(hash); diff --git a/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx b/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx index 8e44f5c7..90105ed9 100644 --- a/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx +++ b/packages/sdk/src/react/ui/components/_internals/action-button/ActionButton.tsx @@ -120,7 +120,7 @@ export const ActionButton = observer( showTransferModal({ collectionAddress: collectionAddress as Hex, chainId: chainId, - collectibleId:tokenId, + collectibleId: tokenId, }) } /> diff --git a/packages/sdk/src/react/ui/modals/BuyModal/_store.ts b/packages/sdk/src/react/ui/modals/BuyModal/_store.ts index bd21309e..0d1723e3 100644 --- a/packages/sdk/src/react/ui/modals/BuyModal/_store.ts +++ b/packages/sdk/src/react/ui/modals/BuyModal/_store.ts @@ -48,7 +48,7 @@ export const initialState: BuyModalState = { state: { order: undefined as unknown as Order, quantity: '1', - modalId: 0 + modalId: 0, }, callbacks: undefined, }; diff --git a/packages/sdk/src/react/ui/modals/BuyModal/index.tsx b/packages/sdk/src/react/ui/modals/BuyModal/index.tsx index 726d3787..87fd901d 100644 --- a/packages/sdk/src/react/ui/modals/BuyModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/BuyModal/index.tsx @@ -32,10 +32,12 @@ export const BuyModal = () => ( ); export const BuyModalContent = () => { - const chainId = String(useSelector(buyModal$.state.order.chainId)) - const collectionAddress = useSelector(buyModal$.state.order.collectionContractAddress) as Hex - const collectibleId = useSelector(buyModal$.state.order.tokenId) - const modalId = useSelector(buyModal$.state.modalId) + const chainId = String(useSelector(buyModal$.state.order.chainId)); + const collectionAddress = useSelector( + buyModal$.state.order.collectionContractAddress, + ) as Hex; + const collectibleId = useSelector(buyModal$.state.order.tokenId); + const modalId = useSelector(buyModal$.state.modalId); const { data: collection } = useCollection({ chainId, @@ -111,38 +113,40 @@ interface ERC1155QuantityModalProps extends CheckoutModalProps { collectibleId: string; } -const ERC1155QuantityModal = observer(({ - buy, - collectable, - order, - chainId, - collectionAddress, - collectibleId -}: ERC1155QuantityModalProps) => { - return ( - buyModal$.close()} - title="Select Quantity" - ctas={[ - { - label: 'Select Quantity', - onClick: () => - buy({ - quantity: buyModal$.state.quantity.get(), - orderId: order.orderId, - collectableDecimals: collectable.decimals || 0, - marketplace: order.marketplace, - }), - }, - ]} - > - - - ); -}); +const ERC1155QuantityModal = observer( + ({ + buy, + collectable, + order, + chainId, + collectionAddress, + collectibleId, + }: ERC1155QuantityModalProps) => { + return ( + buyModal$.close()} + title="Select Quantity" + ctas={[ + { + label: 'Select Quantity', + onClick: () => + buy({ + quantity: buyModal$.state.quantity.get(), + orderId: order.orderId, + collectableDecimals: collectable.decimals || 0, + marketplace: order.marketplace, + }), + }, + ]} + > + + + ); + }, +); diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx index 99b54800..3ae4cba8 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx @@ -1,9 +1,7 @@ import { Box } from '@0xsequence/design-system'; import { Show, observer } from '@legendapp/state/react'; import type { Hash, Hex } from 'viem'; -import { - type ContractType, -} from '../../../_internal'; +import { type ContractType } from '../../../_internal'; import { useCollection } from '../../../hooks'; import { useCreateListing } from '../../../hooks/useCreateListing'; import { @@ -46,147 +44,147 @@ export const CreateListingModal = () => { }; export const Modal = observer(() => { - const state = createListingModal$.get(); - const { collectionAddress, chainId, listingPrice, collectibleId } = state; - const { - data: collection, - isLoading: collectionIsLoading, - isError: collectionIsError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { getListingSteps } = useCreateListing({ - chainId, - collectionAddress, - onTransactionSent: (hash) => { - if (!hash) return; - createListingModal$.close(); - }, - onError: (error) => { - if (typeof createListingModal$.callbacks?.onError === 'function') { - createListingModal$.onError(error); - } else { - console.debug('onError callback not provided:', error); - } - }, - }); - - // biome-ignore lint/suspicious/noExplicitAny: - const handleStepExecution = async (execute?: any) => { - if (!execute) return; - try { - await refreshSteps(); - await execute(); - } catch (error) { - createListingModal$.onError?.(error as Error); + const state = createListingModal$.get(); + const { collectionAddress, chainId, listingPrice, collectibleId } = state; + const { + data: collection, + isLoading: collectionIsLoading, + isError: collectionIsError, + } = useCollection({ + chainId, + collectionAddress, + }); + + const { getListingSteps } = useCreateListing({ + chainId, + collectionAddress, + collectibleId, + onTransactionSent: (hash) => { + if (!hash) return; + createListingModal$.close(); + }, + onError: (error) => { + if (typeof createListingModal$.callbacks?.onError === 'function') { + createListingModal$.onError(error); + } else { + console.debug('onError callback not provided:', error); } - }; - - if (collectionIsLoading) { - return ( - - ); - } - - if (collectionIsError) { - return ( - - ); + }, + }); + + // biome-ignore lint/suspicious/noExplicitAny: + const handleStepExecution = async (execute?: any) => { + if (!execute) return; + try { + await refreshSteps(); + await execute(); + } catch (error) { + createListingModal$.onError?.(error as Error); } + }; - const dateToUnixTime = (date: Date) => - Math.floor(date.getTime() / 1000).toString(); - - const { isLoading, steps, refreshSteps } = getListingSteps({ - // biome-ignore lint/style/noNonNullAssertion: - contractType: collection!.type as ContractType, - listing: { - tokenId: collectibleId, - quantity: createListingModal$.quantity.get(), - expiry: dateToUnixTime(createListingModal$.expiry.get()), - currencyAddress: listingPrice.currency.contractAddress, - pricePerToken: listingPrice.amountRaw, - }, - }); - - const ctas = [ - { - label: 'Approve TOKEN', - onClick: () => handleStepExecution(() => steps?.approval.execute()), - hidden: !steps?.approval.isPending, - pending: steps?.approval.isExecuting, - variant: 'glass' as const, - }, - { - label: 'List item for sale', - onClick: () => handleStepExecution(() => steps?.transaction.execute()), - pending: steps?.transaction.isExecuting || isLoading, - disabled: - steps?.approval.isPending || - listingPrice.amountRaw === '0' || - isLoading, - }, - ] satisfies ActionModalProps['ctas']; + if (collectionIsLoading) { + return ( + + ); + } + if (collectionIsError) { return ( - createListingModal$.close()} + onClose={createListingModal$.close} title="List item for sale" - ctas={ctas} - > - + ); + } + + const dateToUnixTime = (date: Date) => + Math.floor(date.getTime() / 1000).toString(); + + const { isLoading, steps, refreshSteps } = getListingSteps({ + // biome-ignore lint/style/noNonNullAssertion: + contractType: collection!.type as ContractType, + listing: { + tokenId: collectibleId, + quantity: createListingModal$.quantity.get(), + expiry: dateToUnixTime(createListingModal$.expiry.get()), + currencyAddress: listingPrice.currency.contractAddress, + pricePerToken: listingPrice.amountRaw, + }, + }); + + const ctas = [ + { + label: 'Approve TOKEN', + onClick: () => handleStepExecution(() => steps?.approval.execute()), + hidden: !steps?.approval.isPending, + pending: steps?.approval.isExecuting, + variant: 'glass' as const, + }, + { + label: 'List item for sale', + onClick: () => handleStepExecution(() => steps?.transaction.execute()), + pending: steps?.transaction.isExecuting || isLoading, + disabled: + steps?.approval.isPending || + listingPrice.amountRaw === '0' || + isLoading, + }, + ] satisfies ActionModalProps['ctas']; + + return ( + createListingModal$.close()} + title="List item for sale" + ctas={ctas} + > + + + + - - - - {!!listingPrice && ( - - )} - - - {collection?.type === 'ERC1155' && ( - )} + - - - - - ); - }, -); + )} + + + + + + ); +}); diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx index a6c79728..2038bf2b 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx @@ -35,165 +35,164 @@ export const MakeOfferModal = () => { ); }; -const ModalContent = observer( - () => { - const state = makeOfferModal$.get(); - const { collectionAddress, chainId, offerPrice, collectibleId } = state; - const [insufficientBalance, setInsufficientBalance] = useState(false); - - const { - data: collection, - isLoading: collectionIsLoading, - isError: collectionIsError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { isLoading: currenciesIsLoading } = useCurrencies({ - chainId, - collectionAddress, - }); - - const { getMakeOfferSteps } = useMakeOffer({ - chainId, - collectionAddress, - onTransactionSent: (hash) => { - if (!hash) return; - - makeOfferModal$.close(); - }, - onSuccess: (hash) => { - if (typeof makeOfferModal$.callbacks?.onSuccess === 'function') { - makeOfferModal$.callbacks.onSuccess(hash); - } else { - console.debug('onSuccess callback not provided:', hash); - } - }, - onError: (error) => { - if (typeof makeOfferModal$.callbacks?.onError === 'function') { - makeOfferModal$.callbacks.onError(error); - } else { - console.debug('onError callback not provided:', error); - } +const ModalContent = observer(() => { + const state = makeOfferModal$.get(); + const { collectionAddress, chainId, offerPrice, collectibleId } = state; + const [insufficientBalance, setInsufficientBalance] = useState(false); + + const { + data: collection, + isLoading: collectionIsLoading, + isError: collectionIsError, + } = useCollection({ + chainId, + collectionAddress, + }); + + const { isLoading: currenciesIsLoading } = useCurrencies({ + chainId, + collectionAddress, + }); + + const { getMakeOfferSteps } = useMakeOffer({ + chainId, + collectionAddress, + collectibleId, + onTransactionSent: (hash) => { + if (!hash) return; + + makeOfferModal$.close(); + }, + onSuccess: (hash) => { + if (typeof makeOfferModal$.callbacks?.onSuccess === 'function') { + makeOfferModal$.callbacks.onSuccess(hash); + } else { + console.debug('onSuccess callback not provided:', hash); } - }); - - const dateToUnixTime = (date: Date) => - Math.floor(date.getTime() / 1000).toString(); - - const currencyAddress = offerPrice.currency.contractAddress; - - const { isLoading, steps, refreshSteps } = getMakeOfferSteps({ - contractType: collection!.type as ContractType, - offer: { - tokenId: collectibleId, - quantity: makeOfferModal$.quantity.get(), - expiry: dateToUnixTime(makeOfferModal$.expiry.get()), - currencyAddress, - pricePerToken: offerPrice.amountRaw, - }, - }); - - useEffect(() => { - if (!currencyAddress) return; - refreshSteps(); - }, [currencyAddress]); - - if (collectionIsLoading || currenciesIsLoading) { - return ( - - ); - } - - if (collectionIsError) { - return ( - - ); - } - - const handleStepExecution = async (execute?: any) => { - if (!execute) return; - try { - await refreshSteps(); - await execute(); - } catch (error) { - makeOfferModal$.callbacks?.onError?.(error as Error); + }, + onError: (error) => { + if (typeof makeOfferModal$.callbacks?.onError === 'function') { + makeOfferModal$.callbacks.onError(error); + } else { + console.debug('onError callback not provided:', error); } - }; - - const ctas = [ - { - label: 'Approve TOKEN', - onClick: () => handleStepExecution(() => steps?.approval.execute()), - hidden: !steps?.approval.isPending, - pending: steps?.approval.isExecuting, - variant: 'glass' as const, - }, - { - label: 'Make offer', - onClick: () => handleStepExecution(() => steps?.transaction.execute()), - pending: steps?.transaction.isExecuting || isLoading, - disabled: - steps?.approval.isPending || - offerPrice.amountRaw === '0' || - insufficientBalance || - isLoading, - }, - ]; + }, + }); + + const dateToUnixTime = (date: Date) => + Math.floor(date.getTime() / 1000).toString(); + + const currencyAddress = offerPrice.currency.contractAddress; + + const { isLoading, steps, refreshSteps } = getMakeOfferSteps({ + contractType: collection!.type as ContractType, + offer: { + tokenId: collectibleId, + quantity: makeOfferModal$.quantity.get(), + expiry: dateToUnixTime(makeOfferModal$.expiry.get()), + currencyAddress, + pricePerToken: offerPrice.amountRaw, + }, + }); + + useEffect(() => { + if (!currencyAddress) return; + refreshSteps(); + }, [currencyAddress]); + + if (collectionIsLoading || currenciesIsLoading) { + return ( + + ); + } + if (collectionIsError) { return ( - makeOfferModal$.close()} + onClose={makeOfferModal$.close} title="Make an offer" - ctas={ctas} - > - + ); + } + + const handleStepExecution = async (execute?: any) => { + if (!execute) return; + try { + await refreshSteps(); + await execute(); + } catch (error) { + makeOfferModal$.callbacks?.onError?.(error as Error); + } + }; + + const ctas = [ + { + label: 'Approve TOKEN', + onClick: () => handleStepExecution(() => steps?.approval.execute()), + hidden: !steps?.approval.isPending, + pending: steps?.approval.isExecuting, + variant: 'glass' as const, + }, + { + label: 'Make offer', + onClick: () => handleStepExecution(() => steps?.transaction.execute()), + pending: steps?.transaction.isExecuting || isLoading, + disabled: + steps?.approval.isPending || + offerPrice.amountRaw === '0' || + insufficientBalance || + isLoading, + }, + ]; + + return ( + makeOfferModal$.close()} + title="Make an offer" + ctas={ctas} + > + + + setInsufficientBalance(state), + }} + /> + + {collection?.type === ContractType.ERC1155 && ( + + )} - setInsufficientBalance(state), - }} + price={offerPrice} /> + )} - {collection?.type === ContractType.ERC1155 && ( - - )} - - {!!offerPrice && ( - - )} - - - - ); - }, -); + + + ); +}); diff --git a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts index f6561c41..4ef52ffa 100644 --- a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts @@ -9,7 +9,6 @@ export const getSellTransactionTitle = (params: ConfirmationStatus) => { return 'Your sale has failed'; } - if (params.isTimeout) { return 'Your sale is taking longer than expected'; } diff --git a/packages/sdk/src/react/ui/modals/SellModal/index.tsx b/packages/sdk/src/react/ui/modals/SellModal/index.tsx index b1885d8e..ded55210 100644 --- a/packages/sdk/src/react/ui/modals/SellModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/SellModal/index.tsx @@ -6,9 +6,7 @@ import TransactionDetails from '../_internal/components/transactionDetails'; import TransactionHeader from '../_internal/components/transactionHeader'; import { sellModal$ } from './_store'; import { useCollection, useCurrencies } from '../../../hooks'; -import { - type Order, -} from '../../../_internal'; +import { type Order } from '../../../_internal'; import { useSell } from '../../../hooks/useSell'; import { LoadingModal } from '../_internal/components/actionModal/LoadingModal'; import { ErrorModal } from '..//_internal/components/actionModal/ErrorModal'; @@ -35,113 +33,112 @@ export const SellModal = () => { ); }; -const ModalContent = observer( - () => { - const { tokenId, collectionAddress, chainId, order } = sellModal$.get(); - const { sell } = useSell({ - collectionAddress, - chainId, - onTransactionSent: (hash) => { - if (!hash) return; +const ModalContent = observer(() => { + const { tokenId, collectionAddress, chainId, order } = sellModal$.get(); + const { sell } = useSell({ + collectionAddress, + chainId, + collectibleId: tokenId, + onTransactionSent: (hash) => { + if (!hash) return; - sellModal$.close(); - }, - onSuccess: (hash) => { - if (typeof sellModal$.callbacks?.onSuccess === 'function') { - sellModal$.callbacks.onSuccess(hash); - } else { - console.debug('onSuccess callback not provided:', hash); - } - }, - onError: (error) => { - if (typeof sellModal$.callbacks?.onError === 'function') { - sellModal$.callbacks.onError(error); - } else { - console.debug('onError callback not provided:', error); - } + sellModal$.close(); + }, + onSuccess: (hash) => { + if (typeof sellModal$.callbacks?.onSuccess === 'function') { + sellModal$.callbacks.onSuccess(hash); + } else { + console.debug('onSuccess callback not provided:', hash); } - }); - - const { - data: collection, - isLoading: collectionLoading, - isError: collectionError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { data: currencies, isLoading: currenciesLoading } = useCurrencies({ - chainId, - collectionAddress, - }); + }, + onError: (error) => { + if (typeof sellModal$.callbacks?.onError === 'function') { + sellModal$.callbacks.onError(error); + } else { + console.debug('onError callback not provided:', error); + } + }, + }); - if (collectionLoading || currenciesLoading) { - return ( - - ); - } + const { + data: collection, + isLoading: collectionLoading, + isError: collectionError, + } = useCollection({ + chainId, + collectionAddress, + }); - if (collectionError || order === undefined) { - return ( - - ); - } + const { data: currencies, isLoading: currenciesLoading } = useCurrencies({ + chainId, + collectionAddress, + }); - const currency = currencies?.find( - (c) => c.contractAddress === order?.priceCurrencyAddress, + if (collectionLoading || currenciesLoading) { + return ( + ); + } + if (collectionError || order === undefined) { return ( - - sell({ - orderId: order?.orderId, - marketplace: order?.marketplace, - }), - }, - ]} - > - - - + ); + } + + const currency = currencies?.find( + (c) => c.contractAddress === order?.priceCurrencyAddress, + ); + + return ( + + sell({ + orderId: order?.orderId, + marketplace: order?.marketplace, + }), + }, + ]} + > + + + - - ); - }, -); + : undefined + } + currencyImageUrl={currency?.imageUrl} + /> + + ); +}); diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_store.ts b/packages/sdk/src/react/ui/modals/TransferModal/_store.ts index 5181a3b8..502e105e 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_store.ts +++ b/packages/sdk/src/react/ui/modals/TransferModal/_store.ts @@ -23,13 +23,18 @@ export interface TransferModalState { export const initialState: TransferModalState = { isOpen: false, - open: ({ chainId, collectionAddress, collectibleId, callbacks }: ShowTransferModalArgs) => { + open: ({ + chainId, + collectionAddress, + collectibleId, + callbacks, + }: ShowTransferModalArgs) => { transferModal$.state.set({ ...transferModal$.state.get(), chainId, collectionAddress, collectibleId, - callbacks + callbacks, }); transferModal$.isOpen.set(true); }, diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts index 73f01586..6e694486 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts +++ b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts @@ -13,7 +13,6 @@ export const getTransferTransactionTitle = (params: ConfirmationStatus) => { return 'Transfer is taking longer than expected'; } - return 'Transfer is processing'; }; diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx index 13f27b2b..30064e51 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/index.tsx @@ -14,8 +14,13 @@ import { ModalCallbacks } from '../../../_internal/types'; const EnterWalletAddressView = () => { const { address } = useAccount(); - const { collectionAddress, collectibleId, chainId, collectionType, callbacks } = - transferModal$.state.get(); + const { + collectionAddress, + collectibleId, + chainId, + collectionType, + callbacks, + } = transferModal$.state.get(); const $quantity = transferModal$.state.quantity; const isWalletAddressValid = isAddress( transferModal$.state.receiverAddress.get(), @@ -37,7 +42,7 @@ const EnterWalletAddressView = () => { collection?.type as CollectionType | undefined, ); const { transfer } = useHandleTransfer(); - const {show: showTransactionStatusModal} = useTransactionStatusModal(); + const { show: showTransactionStatusModal } = useTransactionStatusModal(); function handleChangeWalletAddress( event: React.ChangeEvent, @@ -46,20 +51,20 @@ const EnterWalletAddressView = () => { } async function handleChangeView() { - transfer().then((hash) => { - - showTransactionStatusModal({ - collectionAddress, - collectibleId, - chainId, - hash: hash!, - callbacks: callbacks as ModalCallbacks, - type:TransactionType.TRANSFER, - }); - } - ).catch(() => { - transferModal$.view.set('enterReceiverAddress'); - }) + transfer() + .then((hash) => { + showTransactionStatusModal({ + collectionAddress, + collectibleId, + chainId, + hash: hash!, + callbacks: callbacks as ModalCallbacks, + type: TransactionType.TRANSFER, + }); + }) + .catch(() => { + transferModal$.view.set('enterReceiverAddress'); + }); transferModal$.view.set('followWalletInstructions'); } diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx index abe2d0c0..1c1afa81 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/_views/enterWalletAddress/useHandleTransfer.tsx @@ -11,7 +11,7 @@ const useHandleTransfer = () => { quantity, chainId, collectionType, - callbacks + callbacks, } = transferModal$.state.get(); const { transferTokensAsync } = useTransferTokens(); diff --git a/packages/sdk/src/react/ui/modals/TransferModal/index.tsx b/packages/sdk/src/react/ui/modals/TransferModal/index.tsx index f68d5b43..6b30bcd2 100644 --- a/packages/sdk/src/react/ui/modals/TransferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/TransferModal/index.tsx @@ -46,13 +46,13 @@ export const useTransferModal = () => { onError: (callbacks: ModalCallbacks) => { transferModal$.state.set({ ...transferModal$.state.get(), - callbacks + callbacks, }); }, onSuccess: (callbacks: ModalCallbacks) => { transferModal$.state.set({ ...transferModal$.state.get(), - callbacks + callbacks, }); }, }; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/alertMessage/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/alertMessage/index.tsx index 977b4168..38ca348c 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/alertMessage/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/alertMessage/index.tsx @@ -10,7 +10,12 @@ type AlertMessageProps = { export default function AlertMessage({ message, type }: AlertMessageProps) { return ( - + {message} diff --git a/playgrounds/react-vite/src/tabs/Collectable.tsx b/playgrounds/react-vite/src/tabs/Collectable.tsx index e256999e..8e5f8965 100644 --- a/playgrounds/react-vite/src/tabs/Collectable.tsx +++ b/playgrounds/react-vite/src/tabs/Collectable.tsx @@ -182,7 +182,7 @@ function Actions({ isOwner }: { isOwner: boolean }) { openTransferModal({ collectionAddress: context.collectionAddress, chainId: context.chainId, - tokenId: context.collectibleId, + collectibleId: context.collectibleId, }) } label="Transfer" @@ -219,6 +219,7 @@ function ListingsTable() { const { cancel } = useCancelOrder({ chainId, collectionAddress, + collectibleId }); const toast = useToast(); @@ -253,7 +254,7 @@ function ListingsTable() { openBuyModal({ collectionAddress: collectionAddress, chainId: chainId, - tokenId: collectibleId, + collectibleId, order: order, }); } From 3e52fc4ecb49d960730af85fdeeae3abfbdb9fb1 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:40:35 +0300 Subject: [PATCH 10/74] implement utils for generating titles and message --- .../util/getFormattedType.ts | 23 ++++++++++ .../transactionStatusModal/util/getMessage.ts | 26 +++++++++++ .../transactionStatusModal/util/getTitle.ts | 43 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getFormattedType.ts create mode 100644 packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts create mode 100644 packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getTitle.ts diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getFormattedType.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getFormattedType.ts new file mode 100644 index 00000000..73731b08 --- /dev/null +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getFormattedType.ts @@ -0,0 +1,23 @@ +import { TransactionType } from '../../../../../../_internal/transaction-machine/execute-transaction'; + +export function getFormattedType( + transactionType: TransactionType, + verb: boolean = false, +): string { + switch (transactionType) { + case TransactionType.TRANSFER: + return verb ? 'transferred' : 'transfer'; + case TransactionType.LISTING: + return verb ? 'listed' : 'listing'; + case TransactionType.BUY: + return verb ? 'purchased' : 'purchase'; + case TransactionType.SELL: + return verb ? 'sold' : 'sale'; + case TransactionType.CANCEL: + return verb ? 'cancelled' : 'cancellation'; + case TransactionType.OFFER: + return verb ? 'offered' : 'offer'; + default: + return 'transaction'; + } +} diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts new file mode 100644 index 00000000..c0dac526 --- /dev/null +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts @@ -0,0 +1,26 @@ +import { TransactionType } from '../../../../../../_internal/transaction-machine/execute-transaction'; +import { TransactionStatus } from '../store'; +import { getFormattedType } from './getFormattedType'; + +export function getTransactionStatusModalMessage({ + transactionStatus, + transactionType, + collectibleName, +}: { + transactionStatus: TransactionStatus; + transactionType: TransactionType; + collectibleName: string; +}): string { + switch (transactionStatus) { + case 'PENDING': + return `You just ${getFormattedType(transactionType, true)} ${collectibleName}. It should be confirmed on the blockchain shortly.`; + case 'SUCCESS': + return `You just ${getFormattedType(transactionType, true)} ${collectibleName}. It’s been confirmed on the blockchain!`; + case 'FAILED': + return `Your ${getFormattedType(transactionType)} has failed.`; + case 'TIMEOUT': + return `Your ${getFormattedType(transactionType)} takes too long. Click the link below to track it on the explorer.`; + default: + return 'Your transaction is processing'; + } +} diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getTitle.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getTitle.ts new file mode 100644 index 00000000..12f53abe --- /dev/null +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getTitle.ts @@ -0,0 +1,43 @@ +import { TransactionType } from '../../../../../../_internal/transaction-machine/execute-transaction'; +import { TransactionStatus } from '../store'; +import { getFormattedType } from './getFormattedType'; + +export function getTransactionStatusModalTitle({ + transactionStatus, + transactionType, +}: { + transactionStatus: TransactionStatus; + transactionType: TransactionType; +}): string { + switch (transactionStatus) { + case 'PENDING': + return `Your ${getFormattedType(transactionType)} is processing`; + case 'SUCCESS': + return `Your ${getFormattedType(transactionType)} has processed`; + case 'FAILED': + return `Your ${getFormattedType(transactionType)} has failed`; + case 'TIMEOUT': + return `Your ${getFormattedType(transactionType)} takes too long`; + default: + return 'Your transaction is processing'; + } +} + +export function getTransactionStatusModalSpinnerTitle({ + transactionStatus, +}: { + transactionStatus: TransactionStatus; +}): string { + switch (transactionStatus) { + case 'PENDING': + return 'Processing transaction'; + case 'SUCCESS': + return 'Transaction completed'; + case 'FAILED': + return 'Transaction failed'; + case 'TIMEOUT': + return 'Taking too long'; + default: + return 'Processing transaction'; + } +} From 229d4e095d5cc52f78a3f562428ec013e304fc6c Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:40:44 +0300 Subject: [PATCH 11/74] remove console.logs --- .../react/_internal/transaction-machine/execute-transaction.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 8b838598..798f5821 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -378,7 +378,6 @@ export class TransactionMachine { } private async executeTransaction(step: Step): Promise { - console.log('executeTransaction', step); const transactionData = { account: this.getAccount(), chain: this.getChainForTransaction(), @@ -389,8 +388,6 @@ export class TransactionMachine { debug('Executing transaction', transactionData); const hash = await this.walletClient.sendTransaction(transactionData); - console.log('hash', hash, this.config.config.collectibleId); - useTransactionStatusModal().show({ chainId: this.getChainId()! as unknown as string, collectionAddress: this.config.config.collectionAddress as Hex, From 17b1090e5ce3ba5116e23f72d6b7c864c3023e94 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:41:22 +0300 Subject: [PATCH 12/74] change transaction status type and use it --- .../useTransactionPreviewTitle.tsx | 4 +- .../transactionStatusModal/index.tsx | 83 +++++++++---------- .../transactionStatusModal/store.ts | 5 +- 3 files changed, 41 insertions(+), 51 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx index 73b1b600..9ce1570c 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/useTransactionPreviewTitle.tsx @@ -1,7 +1,5 @@ import { useMemo } from 'react'; -import type { - ConfirmationStatus, -} from '../transactionStatusModal/store'; +import type { ConfirmationStatus } from '../transactionStatusModal/store'; import { TRANSACTION_TITLES } from './consts'; import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index 62f82317..267c6683 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -16,6 +16,7 @@ import TransactionFooter from '../transaction-footer'; import TransactionPreview from '../transactionPreview'; import { type ConfirmationStatus, + TransactionStatus, transactionStatusModal$, } from './store'; import { @@ -28,6 +29,8 @@ import { getPublicRpcClient } from '../../../../../../utils'; import { TRANSACTION_CONFIRMATIONS_DEFAULT } from '@0xsequence/kit'; import { TransactionType } from '../../../../../_internal/transaction-machine/execute-transaction'; import { ModalCallbacks } from '../../types'; +import { getTransactionStatusModalTitle } from './util/getTitle'; +import { getTransactionStatusModalMessage } from './util/getMessage'; export type ShowTransactionStatusModalArgs = { hash: Hex; @@ -52,40 +55,31 @@ export const useTransactionStatusModal = () => { const TransactionStatusModal = observer(() => { const { + type, hash, price, collectionAddress, chainId, collectibleId, - getTitle, - getMessage, callbacks, queriesToInvalidate, } = transactionStatusModal$.state.get(); - const { data: collectible } = useCollectible({ + const { data: collectible, isLoading: collectibleLoading } = useCollectible({ collectionAddress, chainId, collectibleId, }); - const [transactionStatus, setTransactionStatus] = useState< - 'pending' | 'success' | 'error' | 'timeout' - >('pending'); - const title = - getTitle && - getTitle({ - isConfirmed: transactionStatus === 'success', - isConfirming: transactionStatus === 'pending', - isFailed: transactionStatus === 'error', - isTimeout: transactionStatus === 'timeout', - }); - const message = - getMessage && - getMessage({ - isConfirmed: transactionStatus === 'success', - isConfirming: transactionStatus === 'pending', - isFailed: transactionStatus === 'error', - isTimeout: transactionStatus === 'timeout', - }); + const [transactionStatus, setTransactionStatus] = + useState('PENDING'); + const title = getTransactionStatusModalTitle({ + transactionStatus, + transactionType: type!, + }); + const message = getTransactionStatusModalMessage({ + transactionStatus, + transactionType: type!, + collectibleName: collectible?.name || '', + }); const { onError, onSuccess }: ModalCallbacks = callbacks || {}; const queryClient = getQueryClient(); const publicClient = chainId ? getPublicRpcClient(chainId) : null; @@ -103,25 +97,25 @@ const TransactionStatusModal = observer(() => { ?.then((receipt) => { if (receipt.status === 'success') { console.log('receipt', receipt); - setTransactionStatus('success'); + setTransactionStatus('SUCCESS'); } }) .catch((error) => { if (error instanceof WaitForTransactionReceiptTimeoutError) { - setTransactionStatus('timeout'); + setTransactionStatus('TIMEOUT'); return; } - setTransactionStatus('error'); + setTransactionStatus('FAILED'); }); if (queriesToInvalidate) { queryClient.invalidateQueries({ queryKey: [...queriesToInvalidate] }); } - return () => { - setTransactionStatus('pending'); - } + return () => { + setTransactionStatus('PENDING'); + }; }, [onSuccess, onError, transactionStatusModal$.isOpen.get()]); return ( @@ -151,26 +145,25 @@ const TransactionStatusModal = observer(() => { )} - {collectible && ( - - )} + diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index 2b69281d..59e06123 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -1,4 +1,3 @@ -import type { TransactionStatus } from '@0xsequence/indexer'; import { observable } from '@legendapp/state'; import type { QueryKey } from '@tanstack/react-query'; import type { Hex } from 'viem'; @@ -14,7 +13,7 @@ export type ConfirmationStatus = { isTimeout: boolean; }; -export type TransactionStatusExtended = TransactionStatus | 'PENDING'; +export type TransactionStatus = 'PENDING' | 'SUCCESS' | 'FAILED' | 'TIMEOUT'; export interface TransactionStatusModalState { isOpen: boolean; @@ -22,7 +21,7 @@ export interface TransactionStatusModalState { close: () => void; state: { hash: Hex | undefined; - status: TransactionStatusExtended; + status: TransactionStatus; type: TransactionType | undefined; price: Price | undefined; collectionAddress: Hex; From 214e2ca2db8c3656633fe5d1db30b51610cae2ac Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:41:48 +0300 Subject: [PATCH 13/74] handle loading collectible preview with skeleton to avoid layout flashing --- .../components/transactionPreview/index.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx index 39990971..da253f7d 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx @@ -1,4 +1,10 @@ -import { Box, Image, NetworkImage, Text } from '@0xsequence/design-system'; +import { + Box, + Image, + NetworkImage, + Skeleton, + Text, +} from '@0xsequence/design-system'; import { observer } from '@legendapp/state/react'; import { type Hex, formatUnits } from 'viem'; import type { Price } from '../../../../../../types'; @@ -13,7 +19,8 @@ type TransactionPreviewProps = { price?: Price; collectionAddress: Hex; chainId: string; - collectible: TokenMetadata; + collectible: TokenMetadata | undefined; + collectibleLoading: boolean; currencyImageUrl?: string; isConfirming: boolean; isConfirmed: boolean; @@ -27,29 +34,38 @@ const TransactionPreview = observer( collectionAddress, chainId, collectible, + collectibleLoading, currencyImageUrl, isConfirming, isConfirmed, isFailed, - isTimeout + isTimeout, }: TransactionPreviewProps) => { const { type } = transactionStatusModal$.state.get(); const title = useTransactionPreviewTitle( { isConfirmed, isConfirming, isFailed, isTimeout }, type, ); - const { data: collection } = useCollection({ + const { data: collection, isLoading: collectionLoading } = useCollection({ collectionAddress, chainId, }); - const collectibleImage = collectible.image; - const collectibleName = collectible.name; + const collectibleImage = collectible?.image; + const collectibleName = collectible?.name; const collectionName = collection?.name; const priceFormatted = price ? formatUnits(BigInt(price!.amountRaw), price!.currency.decimals) : undefined; + if (collectibleLoading || collectionLoading) { + return ( + + + + ); + } + return ( From f1c2b98e9cf478dc4c97d80396b7d540f500da44 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 10:43:32 +0300 Subject: [PATCH 14/74] remove unused utils about modal spesific message generation --- ...getCreateListingTransactionTitleMessage.ts | 38 ------------------- .../getMakeOfferTransactionTitleMessage.ts | 36 ------------------ .../_utils/getSellTransactionTitleMessage.ts | 36 ------------------ .../getTransferTransactionTitleMessage.ts | 36 ------------------ 4 files changed, 146 deletions(-) delete mode 100644 packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts delete mode 100644 packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts delete mode 100644 packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts delete mode 100644 packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts deleted file mode 100644 index f15655ec..00000000 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { ConfirmationStatus } from '../../_internal/components/transactionStatusModal/store'; - -export const getCreateListingTransactionTitle = ( - params: ConfirmationStatus, -) => { - if (params.isConfirmed) { - return 'Listing has processed'; - } - - if (params.isFailed) { - return 'Listing has failed'; - } - - if (params.isTimeout) { - return 'Your listing is taking longer than expected'; - } - - return 'Listing is processing'; -}; - -export const getCreateListingTransactionMessage = ( - params: ConfirmationStatus, - collectibleName: string, -) => { - if (params.isConfirmed) { - return `You just listed ${collectibleName}. It’s been confirmed on the blockchain!`; - } - - if (params.isFailed) { - return `Your listing of ${collectibleName} has failed. Please try again.`; - } - - if (params.isTimeout) { - return `Your listing is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; - } - - return `You just listed ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts deleted file mode 100644 index 022e30cf..00000000 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConfirmationStatus } from '../../_internal/components/transactionStatusModal/store'; - -export const getMakeOfferTransactionTitle = (params: ConfirmationStatus) => { - if (params.isConfirmed) { - return 'Your offer has processed'; - } - - if (params.isFailed) { - return 'Your offer has failed'; - } - - if (params.isTimeout) { - return 'Your offer is taking longer than expected'; - } - - return 'Your offer is processing'; -}; - -export const getMakeOfferTransactionMessage = ( - params: ConfirmationStatus, - collectibleName: string, -) => { - if (params.isConfirmed) { - return `You just made offer for ${collectibleName}. It’s been confirmed on the blockchain!`; - } - - if (params.isFailed) { - return `Your offer for ${collectibleName} has failed. Please try again.`; - } - - if (params.isTimeout) { - return `Your offer is still being processed. This may take a little longer than usual. Please continue with transaction hash below to check the status on explorer.`; - } - - return `You just made offer for ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts deleted file mode 100644 index 4ef52ffa..00000000 --- a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConfirmationStatus } from '../../_internal/components/transactionStatusModal/store'; - -export const getSellTransactionTitle = (params: ConfirmationStatus) => { - if (params.isConfirmed) { - return 'Your sale has processed'; - } - - if (params.isFailed) { - return 'Your sale has failed'; - } - - if (params.isTimeout) { - return 'Your sale is taking longer than expected'; - } - - return 'Your sale is processing'; -}; - -export const getSellTransactionMessage = ( - params: ConfirmationStatus, - collectibleName: string, -) => { - if (params.isConfirmed) { - return `You just sold ${collectibleName}. It’s been confirmed on the blockchain!`; - } - - if (params.isFailed) { - return `Your sale of ${collectibleName} has failed. Please try again.`; - } - - if (params.isTimeout) { - return `Your sale is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; - } - - return `You just sold ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts deleted file mode 100644 index 6e694486..00000000 --- a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ConfirmationStatus } from '../../_internal/components/transactionStatusModal/store'; - -export const getTransferTransactionTitle = (params: ConfirmationStatus) => { - if (params.isConfirmed) { - return 'Transfer has processed'; - } - - if (params.isFailed) { - return 'Transfer has failed'; - } - - if (params.isTimeout) { - return 'Transfer is taking longer than expected'; - } - - return 'Transfer is processing'; -}; - -export const getTransferTransactionMessage = ( - params: ConfirmationStatus, - collectibleName: string, -) => { - if (params.isConfirmed) { - return `You just tranferred ${collectibleName}. It’s been confirmed on the blockchain!`; - } - - if (params.isFailed) { - return `Transferring ${collectibleName} has failed. Please try again.`; - } - - if (params.isTimeout) { - return `Transfer is still being processed. This may take a little longer than usual. Please continue with the transaction hash below to check the status on explorer.`; - } - - return `You just transferred ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; From ef51f85bc056e01d61c47b8f676152b9e5ce1330 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 11:12:36 +0300 Subject: [PATCH 15/74] avoid misunderstanding on message --- .../components/transactionStatusModal/util/getMessage.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts index c0dac526..0a054059 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts @@ -11,11 +11,14 @@ export function getTransactionStatusModalMessage({ transactionType: TransactionType; collectibleName: string; }): string { + // without this, the text will be "Your cancellation CollectibleXXX has failed." which sounds weird + const hideCollectibleName = transactionType === 'CANCEL' + switch (transactionStatus) { case 'PENDING': - return `You just ${getFormattedType(transactionType, true)} ${collectibleName}. It should be confirmed on the blockchain shortly.`; + return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}`: ''}. It should be confirmed on the blockchain shortly.`; case 'SUCCESS': - return `You just ${getFormattedType(transactionType, true)} ${collectibleName}. It’s been confirmed on the blockchain!`; + return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}`: ''}. It’s been confirmed on the blockchain!`; case 'FAILED': return `Your ${getFormattedType(transactionType)} has failed.`; case 'TIMEOUT': From 99947748dae0a7462ea440bf67e1fa8efee6b6e2 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 11:13:15 +0300 Subject: [PATCH 16/74] pass callbacks to TransactionStatusModal on execute-transaction --- .../_internal/transaction-machine/execute-transaction.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 798f5821..8f81ed8d 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -394,6 +394,10 @@ export class TransactionMachine { collectibleId: this.config.config.collectibleId as string, hash: hash as Hash, type: this.config.config.type, + callbacks: { + onError: this.config.onError, + onSuccess: this.config.onSuccess, + } }); debug('Transaction submitted', { hash }); From 8401c278f67e6196227ef3fa6bf8f89a8c830795 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 11:13:50 +0300 Subject: [PATCH 17/74] set optional `confirmations` for ShowTransactionStatusModalArgs --- .../_internal/components/transactionStatusModal/index.tsx | 4 +++- .../_internal/components/transactionStatusModal/store.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index 267c6683..cc4d2d67 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -43,6 +43,7 @@ export type ShowTransactionStatusModalArgs = { type: TransactionType; callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; + confirmations?: number; }; export const useTransactionStatusModal = () => { @@ -63,6 +64,7 @@ const TransactionStatusModal = observer(() => { collectibleId, callbacks, queriesToInvalidate, + confirmations } = transactionStatusModal$.state.get(); const { data: collectible, isLoading: collectibleLoading } = useCollectible({ collectionAddress, @@ -85,7 +87,7 @@ const TransactionStatusModal = observer(() => { const publicClient = chainId ? getPublicRpcClient(chainId) : null; const waitForTransactionReceiptPromise = publicClient?.waitForTransactionReceipt({ - confirmations: TRANSACTION_CONFIRMATIONS_DEFAULT, + confirmations: confirmations || TRANSACTION_CONFIRMATIONS_DEFAULT, hash: hash!, }); diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index 59e06123..5c4d9b58 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -31,6 +31,7 @@ export interface TransactionStatusModalState { getMessage?: (params: ConfirmationStatus) => string; callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; + confirmations?: number; }; } @@ -47,6 +48,7 @@ export const initialState: TransactionStatusModalState = { type, callbacks, queriesToInvalidate, + confirmations }) => { transactionStatusModal$.state.set({ ...transactionStatusModal$.state.get(), @@ -60,6 +62,7 @@ export const initialState: TransactionStatusModalState = { type, callbacks, queriesToInvalidate, + confirmations }); transactionStatusModal$.isOpen.set(true); }, @@ -80,6 +83,8 @@ export const initialState: TransactionStatusModalState = { getMessage: undefined, type: undefined, callbacks: undefined, + queriesToInvalidate: [], + confirmations: -1 }, }; From 89b40ffda3a224a8e617be871abe6d50887b9f17 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 11:51:32 +0300 Subject: [PATCH 18/74] block showing status modal to not show while approving --- .../transaction-machine/execute-transaction.ts | 8 +++++--- .../components/transactionStatusModal/index.tsx | 8 ++++++-- .../components/transactionStatusModal/store.ts | 10 +++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 8f81ed8d..355f6d9c 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -377,7 +377,7 @@ export class TransactionMachine { this.config.onSuccess?.(hash); } - private async executeTransaction(step: Step): Promise { + private async executeTransaction(step: Step, statusModalShown?:boolean): Promise { const transactionData = { account: this.getAccount(), chain: this.getChainForTransaction(), @@ -397,7 +397,8 @@ export class TransactionMachine { callbacks: { onError: this.config.onError, onSuccess: this.config.onSuccess, - } + }, + blocked: !statusModalShown, }); debug('Transaction submitted', { hash }); @@ -549,7 +550,8 @@ export class TransactionMachine { const hash = await this.executeTransaction(step); return { hash }; } else { - const hash = await this.executeTransaction(step); + // status modal is shown when executing listing/offer/cancel etc. steps, not approval steps + const hash = await this.executeTransaction(step, true); this.config.onSuccess?.(hash); return { hash }; } diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index cc4d2d67..a18fecad 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -44,12 +44,16 @@ export type ShowTransactionStatusModalArgs = { callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; confirmations?: number; + blocked?: boolean; }; export const useTransactionStatusModal = () => { return { - show: (args: ShowTransactionStatusModalArgs) => - transactionStatusModal$.open(args), + show: (args: ShowTransactionStatusModalArgs) =>{ + if (args.blocked) return; + + transactionStatusModal$.open(args); + }, close: () => transactionStatusModal$.close(), }; }; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index 5c4d9b58..26090d94 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -32,6 +32,7 @@ export interface TransactionStatusModalState { callbacks?: ModalCallbacks; queriesToInvalidate?: QueryKey[]; confirmations?: number; + blocked?: boolean; }; } @@ -48,7 +49,8 @@ export const initialState: TransactionStatusModalState = { type, callbacks, queriesToInvalidate, - confirmations + confirmations, + blocked }) => { transactionStatusModal$.state.set({ ...transactionStatusModal$.state.get(), @@ -62,7 +64,8 @@ export const initialState: TransactionStatusModalState = { type, callbacks, queriesToInvalidate, - confirmations + confirmations, + blocked }); transactionStatusModal$.isOpen.set(true); }, @@ -84,7 +87,8 @@ export const initialState: TransactionStatusModalState = { type: undefined, callbacks: undefined, queriesToInvalidate: [], - confirmations: -1 + confirmations: -1, + blocked: false, }, }; From a5137781df7b4348475000df55f875b6b7882223 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 12:02:24 +0300 Subject: [PATCH 19/74] add objectFit style to image components in token and transaction previews --- .../react/ui/modals/_internal/components/tokenPreview/index.tsx | 1 + .../ui/modals/_internal/components/transactionPreview/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/tokenPreview/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/tokenPreview/index.tsx index 9ef0f6b7..2ff24552 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/tokenPreview/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/tokenPreview/index.tsx @@ -44,6 +44,7 @@ export default function TokenPreview({ width={'9'} height={'9'} borderRadius={'xs'} + style={{ objectFit: 'cover' }} /> diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx index da253f7d..2e3e70e0 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionPreview/index.tsx @@ -92,6 +92,7 @@ const TransactionPreview = observer( height="9" borderRadius="xs" marginRight="3" + style={{ objectFit: 'cover' }} /> Date: Fri, 6 Dec 2024 12:12:22 +0300 Subject: [PATCH 20/74] dont call onTransactionSent and onSuccess after token approval --- .../transaction-machine/execute-transaction.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 355f6d9c..6c1ea144 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -367,14 +367,21 @@ export class TransactionMachine { await this.transition(TransactionState.SUCCESS); return; } + await this.transition(TransactionState.CONFIRMING); - this.config.onTransactionSent?.(hash); + + if(this.currentState !== TransactionState.TOKEN_APPROVAL) { + this.config.onTransactionSent?.(hash); + } const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); debug('Transaction confirmed', receipt); await this.transition(TransactionState.SUCCESS); - this.config.onSuccess?.(hash); + + if(this.currentState === TransactionState.TOKEN_APPROVAL) { + this.config.onSuccess?.(hash); + } } private async executeTransaction(step: Step, statusModalShown?:boolean): Promise { From abe11abb53609c93f5f2ae411a5ed50a6ec4ac28 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 14:12:42 +0300 Subject: [PATCH 21/74] * improve naming for token approval base, * implement listenApprovalReceipt, * implement approve, --- .../execute-transaction.ts | 94 ++++++++++++++----- .../ui/modals/CreateListingModal/index.tsx | 7 +- .../react/ui/modals/MakeOfferModal/index.tsx | 17 ++-- .../components/actionModal/ActionModal.tsx | 4 +- 4 files changed, 90 insertions(+), 32 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 6c1ea144..455a5a47 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -33,8 +33,8 @@ export enum TransactionState { SWITCH_CHAIN = 'SWITCH_CHAIN', CHECKING_STEPS = 'CHECKING_STEPS', TOKEN_APPROVAL = 'TOKEN_APPROVAL', + TOKEN_APPROVED = 'APPROVED_TOKEN', EXECUTING_TRANSACTION = 'EXECUTING_TRANSACTION', - CONFIRMING = 'CONFIRMING', SUCCESS = 'SUCCESS', ERROR = 'ERROR', } @@ -124,7 +124,7 @@ interface StateConfig { } interface TransactionStep { - isPending: boolean; + isReadyToExecute: boolean; isExecuting: boolean; } @@ -133,13 +133,15 @@ export interface TransactionSteps { execute: () => Promise; }; approval: TransactionStep & { - execute: () => + approve: () => | Promise<{ hash: Hash } | undefined> | Promise | undefined; + approved?: boolean; }; transaction: TransactionStep & { execute: () => Promise<{ hash: Hash } | undefined> | Promise; + done?: boolean; }; } @@ -368,23 +370,36 @@ export class TransactionMachine { return; } - await this.transition(TransactionState.CONFIRMING); - - if(this.currentState !== TransactionState.TOKEN_APPROVAL) { - this.config.onTransactionSent?.(hash); - } + this.config.onTransactionSent?.(hash); const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); debug('Transaction confirmed', receipt); await this.transition(TransactionState.SUCCESS); - - if(this.currentState === TransactionState.TOKEN_APPROVAL) { - this.config.onSuccess?.(hash); + + this.config.onSuccess?.(hash); + } + + private async listenApprovalReceipt(hash: Hash) { + try { + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + }); + debug('Approval confirmed', receipt); + await this.transition(TransactionState.TOKEN_APPROVED); + } catch (error) { + await this.transition(TransactionState.ERROR); + throw error; } } - private async executeTransaction(step: Step, statusModalShown?:boolean): Promise { + private async executeTransaction({ + step, + isTokenApproval, + }: { + step: Step; + isTokenApproval: boolean; + }): Promise { const transactionData = { account: this.getAccount(), chain: this.getChainForTransaction(), @@ -405,10 +420,16 @@ export class TransactionMachine { onError: this.config.onError, onSuccess: this.config.onSuccess, }, - blocked: !statusModalShown, + blocked: isTokenApproval, }); debug('Transaction submitted', { hash }); + + if (isTokenApproval) { + await this.listenApprovalReceipt(hash); + return hash; + } + await this.handleTransactionSuccess(hash); return hash; } @@ -541,6 +562,7 @@ export class TransactionMachine { step: Step; props: TransactionInput['props']; }) { + console.log('step:', step); debug('Executing step', { stepId: step.id }); if (!step.to && !step.signature) { throw new Error('Invalid step data'); @@ -554,11 +576,16 @@ export class TransactionMachine { await this.executeSignature(step); } else if (step.id === StepType.tokenApproval) { //TODO: Add some sort ofs callback heres - const hash = await this.executeTransaction(step); + const hash = await this.executeTransaction({ + step, + isTokenApproval: true, + }); return { hash }; } else { - // status modal is shown when executing listing/offer/cancel etc. steps, not approval steps - const hash = await this.executeTransaction(step, true); + const hash = await this.executeTransaction({ + step, + isTokenApproval: false, + }); this.config.onSuccess?.(hash); return { hash }; } @@ -568,6 +595,30 @@ export class TransactionMachine { } } + private async approve({ step }: { step: Step }) { + debug('Executing step', { stepId: step.id }); + if (!step.to && !step.signature) { + throw new Error('Invalid step data'); + } + + if (step.id !== StepType.tokenApproval) { + throw new Error('Invalid approval step'); + } + + try { + await this.switchChain(); + + const hash = await this.executeTransaction({ + step, + isTokenApproval: true, + }); + return { hash }; + } catch (error) { + this.config.onError?.(error as Error); + throw error; + } + } + async getTransactionSteps( props: TransactionInput['props'], ): Promise { @@ -604,18 +655,19 @@ export class TransactionMachine { this.lastProps = props; this.memoizedSteps = { switchChain: { - isPending: !this.isOnCorrectChain(), + isReadyToExecute: !this.isOnCorrectChain(), isExecuting: this.currentState === TransactionState.SWITCH_CHAIN, execute: () => this.switchChain(), }, approval: { - isPending: Boolean(approvalStep), + isReadyToExecute: Boolean(approvalStep), isExecuting: this.currentState === TransactionState.TOKEN_APPROVAL, - execute: () => - approvalStep && this.executeStep({ step: approvalStep, props }), + approved: this.currentState === TransactionState.TOKEN_APPROVED, + approve: () => approvalStep && this.approve({ step: approvalStep }), }, transaction: { - isPending: Boolean(executionStep), + isReadyToExecute: Boolean(executionStep), + done: this.currentState === TransactionState.SUCCESS, isExecuting: this.currentState === TransactionState.EXECUTING_TRANSACTION, execute: () => this.executeStep({ step: executionStep, props }), diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx index 3ae4cba8..b7f88319 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx @@ -121,8 +121,8 @@ export const Modal = observer(() => { const ctas = [ { label: 'Approve TOKEN', - onClick: () => handleStepExecution(() => steps?.approval.execute()), - hidden: !steps?.approval.isPending, + onClick: () => handleStepExecution(() => steps?.approval.approve()), + hidden: !steps?.approval.isExecuting || steps?.approval.approved, pending: steps?.approval.isExecuting, variant: 'glass' as const, }, @@ -131,7 +131,8 @@ export const Modal = observer(() => { onClick: () => handleStepExecution(() => steps?.transaction.execute()), pending: steps?.transaction.isExecuting || isLoading, disabled: - steps?.approval.isPending || + steps?.approval.isExecuting || + steps?.approval.isReadyToExecute || listingPrice.amountRaw === '0' || isLoading, }, diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx index 2038bf2b..b38249f2 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx @@ -84,7 +84,11 @@ const ModalContent = observer(() => { const currencyAddress = offerPrice.currency.contractAddress; - const { isLoading, steps, refreshSteps } = getMakeOfferSteps({ + const { + isLoading: makeOfferStepsLoading, + steps, + refreshSteps, + } = getMakeOfferSteps({ contractType: collection!.type as ContractType, offer: { tokenId: collectibleId, @@ -133,20 +137,21 @@ const ModalContent = observer(() => { const ctas = [ { label: 'Approve TOKEN', - onClick: () => handleStepExecution(() => steps?.approval.execute()), - hidden: !steps?.approval.isPending, + onClick: () => handleStepExecution(() => steps?.approval.approve()), + hidden: !steps?.approval.isReadyToExecute || steps?.approval.approved, pending: steps?.approval.isExecuting, variant: 'glass' as const, }, { label: 'Make offer', onClick: () => handleStepExecution(() => steps?.transaction.execute()), - pending: steps?.transaction.isExecuting || isLoading, + pending: steps?.transaction.isExecuting || makeOfferStepsLoading, disabled: - steps?.approval.isPending || + steps?.approval.isReadyToExecute || offerPrice.amountRaw === '0' || + steps?.approval.isExecuting || insufficientBalance || - isLoading, + makeOfferStepsLoading, }, ]; diff --git a/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx b/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx index 335b85a8..78ae9eda 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx @@ -9,6 +9,7 @@ import { CloseIcon, IconButton, Text, + Spinner, } from '@0xsequence/design-system'; import type { Observable } from '@legendapp/state'; import { observer } from '@legendapp/state/react'; @@ -74,11 +75,10 @@ export const ActionModal = observer( className={ctaStyle} onClick={cta.onClick} variant={cta.variant || 'primary'} - pending={cta.pending} disabled={cta.disabled} size="lg" width="full" - label={cta.label} + label={cta.pending ? : cta.label} /> ), )} From 398a0e89f2c799e79f7e47e69437ac1ae40f82a4 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 14:12:49 +0300 Subject: [PATCH 22/74] format --- .../_internal/components/transactionStatusModal/index.tsx | 4 ++-- .../_internal/components/transactionStatusModal/store.ts | 4 ++-- .../components/transactionStatusModal/util/getMessage.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx index a18fecad..81f21720 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/index.tsx @@ -49,7 +49,7 @@ export type ShowTransactionStatusModalArgs = { export const useTransactionStatusModal = () => { return { - show: (args: ShowTransactionStatusModalArgs) =>{ + show: (args: ShowTransactionStatusModalArgs) => { if (args.blocked) return; transactionStatusModal$.open(args); @@ -68,7 +68,7 @@ const TransactionStatusModal = observer(() => { collectibleId, callbacks, queriesToInvalidate, - confirmations + confirmations, } = transactionStatusModal$.state.get(); const { data: collectible, isLoading: collectibleLoading } = useCollectible({ collectionAddress, diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts index 26090d94..5370b29a 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/store.ts @@ -50,7 +50,7 @@ export const initialState: TransactionStatusModalState = { callbacks, queriesToInvalidate, confirmations, - blocked + blocked, }) => { transactionStatusModal$.state.set({ ...transactionStatusModal$.state.get(), @@ -65,7 +65,7 @@ export const initialState: TransactionStatusModalState = { callbacks, queriesToInvalidate, confirmations, - blocked + blocked, }); transactionStatusModal$.isOpen.set(true); }, diff --git a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts index 0a054059..4456c170 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts +++ b/packages/sdk/src/react/ui/modals/_internal/components/transactionStatusModal/util/getMessage.ts @@ -11,14 +11,14 @@ export function getTransactionStatusModalMessage({ transactionType: TransactionType; collectibleName: string; }): string { - // without this, the text will be "Your cancellation CollectibleXXX has failed." which sounds weird - const hideCollectibleName = transactionType === 'CANCEL' + // without this, the text will be "Your cancellation CollectibleXXX has failed." which sounds weird + const hideCollectibleName = transactionType === 'CANCEL'; switch (transactionStatus) { case 'PENDING': - return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}`: ''}. It should be confirmed on the blockchain shortly.`; + return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}` : ''}. It should be confirmed on the blockchain shortly.`; case 'SUCCESS': - return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}`: ''}. It’s been confirmed on the blockchain!`; + return `You just ${getFormattedType(transactionType, true)}${!hideCollectibleName ? ` ${collectibleName}` : ''}. It’s been confirmed on the blockchain!`; case 'FAILED': return `Your ${getFormattedType(transactionType)} has failed.`; case 'TIMEOUT': From 1406726c556f1cda12008511d78e0fdb7d46d48e Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 14:24:15 +0300 Subject: [PATCH 23/74] separate ApprovalStep --- .../execute-transaction.ts | 21 ++++++++++++------- .../ui/modals/CreateListingModal/index.tsx | 8 +++---- .../react/ui/modals/MakeOfferModal/index.tsx | 8 +++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 455a5a47..3766793a 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -126,22 +126,27 @@ interface StateConfig { interface TransactionStep { isReadyToExecute: boolean; isExecuting: boolean; + executed?: boolean; +} + +interface ApprovalStep { + isReadyToApprove: boolean; + isApproving: boolean; + approved?: boolean; } export interface TransactionSteps { switchChain: TransactionStep & { execute: () => Promise; }; - approval: TransactionStep & { + approval: ApprovalStep & { approve: () => | Promise<{ hash: Hash } | undefined> | Promise | undefined; - approved?: boolean; }; transaction: TransactionStep & { execute: () => Promise<{ hash: Hash } | undefined> | Promise; - done?: boolean; }; } @@ -660,17 +665,17 @@ export class TransactionMachine { execute: () => this.switchChain(), }, approval: { - isReadyToExecute: Boolean(approvalStep), - isExecuting: this.currentState === TransactionState.TOKEN_APPROVAL, - approved: this.currentState === TransactionState.TOKEN_APPROVED, + isReadyToApprove: Boolean(approvalStep), approve: () => approvalStep && this.approve({ step: approvalStep }), + isApproving: this.currentState === TransactionState.TOKEN_APPROVAL, + approved: this.currentState === TransactionState.TOKEN_APPROVED, }, transaction: { isReadyToExecute: Boolean(executionStep), - done: this.currentState === TransactionState.SUCCESS, + execute: () => this.executeStep({ step: executionStep, props }), isExecuting: this.currentState === TransactionState.EXECUTING_TRANSACTION, - execute: () => this.executeStep({ step: executionStep, props }), + executed: this.currentState === TransactionState.SUCCESS, }, } as const; diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx index b7f88319..7d662f8f 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx @@ -122,8 +122,8 @@ export const Modal = observer(() => { { label: 'Approve TOKEN', onClick: () => handleStepExecution(() => steps?.approval.approve()), - hidden: !steps?.approval.isExecuting || steps?.approval.approved, - pending: steps?.approval.isExecuting, + hidden: !steps?.approval.isApproving || steps?.approval.approved, + pending: steps?.approval.isApproving || isLoading, variant: 'glass' as const, }, { @@ -131,8 +131,8 @@ export const Modal = observer(() => { onClick: () => handleStepExecution(() => steps?.transaction.execute()), pending: steps?.transaction.isExecuting || isLoading, disabled: - steps?.approval.isExecuting || - steps?.approval.isReadyToExecute || + steps?.approval.isReadyToApprove || + steps?.approval.isApproving || listingPrice.amountRaw === '0' || isLoading, }, diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx index b38249f2..57ff1c63 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx @@ -138,8 +138,8 @@ const ModalContent = observer(() => { { label: 'Approve TOKEN', onClick: () => handleStepExecution(() => steps?.approval.approve()), - hidden: !steps?.approval.isReadyToExecute || steps?.approval.approved, - pending: steps?.approval.isExecuting, + hidden: !steps?.approval.isReadyToApprove || steps?.approval.approved, + pending: steps?.approval.isApproving || makeOfferStepsLoading, variant: 'glass' as const, }, { @@ -147,9 +147,9 @@ const ModalContent = observer(() => { onClick: () => handleStepExecution(() => steps?.transaction.execute()), pending: steps?.transaction.isExecuting || makeOfferStepsLoading, disabled: - steps?.approval.isReadyToExecute || + steps?.approval.isReadyToApprove || offerPrice.amountRaw === '0' || - steps?.approval.isExecuting || + steps?.approval.isReadyToApprove || insufficientBalance || makeOfferStepsLoading, }, From 9ed7c273ca69b1ae03c1a00f62f384defe9102ea Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Fri, 6 Dec 2024 16:27:36 +0300 Subject: [PATCH 24/74] wrap approve logic with promise --- .../execute-transaction.ts | 74 +++++++++---------- .../react/ui/modals/MakeOfferModal/index.tsx | 11 ++- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index 3766793a..f641ea29 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -4,6 +4,7 @@ import type { Hash, Hex, PublicClient, + TransactionReceipt, TypedDataDomain, WalletClient, } from 'viem'; @@ -141,7 +142,7 @@ export interface TransactionSteps { }; approval: ApprovalStep & { approve: () => - | Promise<{ hash: Hash } | undefined> + | Promise<{ receipt: TransactionReceipt } | undefined> | Promise | undefined; }; @@ -385,19 +386,6 @@ export class TransactionMachine { this.config.onSuccess?.(hash); } - private async listenApprovalReceipt(hash: Hash) { - try { - const receipt = await this.publicClient.waitForTransactionReceipt({ - hash, - }); - debug('Approval confirmed', receipt); - await this.transition(TransactionState.TOKEN_APPROVED); - } catch (error) { - await this.transition(TransactionState.ERROR); - throw error; - } - } - private async executeTransaction({ step, isTokenApproval, @@ -430,11 +418,6 @@ export class TransactionMachine { debug('Transaction submitted', { hash }); - if (isTokenApproval) { - await this.listenApprovalReceipt(hash); - return hash; - } - await this.handleTransactionSuccess(hash); return hash; } @@ -591,7 +574,6 @@ export class TransactionMachine { step, isTokenApproval: false, }); - this.config.onSuccess?.(hash); return { hash }; } } catch (error) { @@ -600,28 +582,42 @@ export class TransactionMachine { } } - private async approve({ step }: { step: Step }) { - debug('Executing step', { stepId: step.id }); - if (!step.to && !step.signature) { - throw new Error('Invalid step data'); - } + private async approve({ + step, + }: { step: Step }): Promise<{ receipt: TransactionReceipt }> { + return new Promise(async (resolve, reject) => { + try { + await this.transition(TransactionState.TOKEN_APPROVAL); + + debug('Executing step', { stepId: step.id }); + if (!step.to && !step.signature) { + throw new Error('Invalid step data'); + } - if (step.id !== StepType.tokenApproval) { - throw new Error('Invalid approval step'); - } + if (step.id !== StepType.tokenApproval) { + throw new Error('Invalid approval step'); + } - try { - await this.switchChain(); + await this.switchChain(); - const hash = await this.executeTransaction({ - step, - isTokenApproval: true, - }); - return { hash }; - } catch (error) { - this.config.onError?.(error as Error); - throw error; - } + const hash = await this.executeTransaction({ + step, + isTokenApproval: true, + }); + + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + }); + + debug('Approval confirmed', receipt); + await this.transition(TransactionState.TOKEN_APPROVED); + + resolve({ receipt }); + } catch (error) { + this.config.onError?.(error as Error); + reject(error); + } + }); } async getTransactionSteps( diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx index 57ff1c63..7e19a28c 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx @@ -130,14 +130,21 @@ const ModalContent = observer(() => { await refreshSteps(); await execute(); } catch (error) { - makeOfferModal$.callbacks?.onError?.(error as Error); + if (typeof makeOfferModal$.callbacks?.onError === 'function') { + makeOfferModal$.callbacks.onError(error as Error); + } else { + console.debug('onError callback not provided:', error); + } } }; const ctas = [ { label: 'Approve TOKEN', - onClick: () => handleStepExecution(() => steps?.approval.approve()), + onClick: () => + handleStepExecution(() => + steps?.approval.approve()?.then(refreshSteps), + ), hidden: !steps?.approval.isReadyToApprove || steps?.approval.approved, pending: steps?.approval.isApproving || makeOfferStepsLoading, variant: 'glass' as const, From 2f2a48838bdb1d163de8f8be845faea115064fe4 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Mon, 9 Dec 2024 13:57:58 +0300 Subject: [PATCH 25/74] change the way Transaction machine works, manage state in detail --- .../transaction-machine/asyncResultHandler.ts | 53 ++ .../execute-transaction.ts | 607 ++++++++++-------- .../useTransactionMachine.ts | 16 +- 3 files changed, 410 insertions(+), 266 deletions(-) create mode 100644 packages/sdk/src/react/_internal/transaction-machine/asyncResultHandler.ts diff --git a/packages/sdk/src/react/_internal/transaction-machine/asyncResultHandler.ts b/packages/sdk/src/react/_internal/transaction-machine/asyncResultHandler.ts new file mode 100644 index 00000000..e0331a37 --- /dev/null +++ b/packages/sdk/src/react/_internal/transaction-machine/asyncResultHandler.ts @@ -0,0 +1,53 @@ +export type Result = { + data?: T; + error?: Error; + isLoading: boolean; + isComplete: boolean; +}; + +export default class AsyncResultHandler { + private state: Result = { + isLoading: false, + isComplete: false, + }; + + constructor(private onError?: (error: Error) => void) {} + + getState(): Result { + return this.state; + } + + async execute(operation: () => Promise): Promise> { + try { + this.state = { + ...this.state, + isLoading: true, + isComplete: false, + error: undefined, + }; + + const data = await operation(); + + this.state = { + data, + isLoading: false, + isComplete: true, + }; + + return this.state; + } catch (error) { + const err = error as Error; + this.state = { + isLoading: false, + isComplete: true, + error: err, + }; + + if (this.onError) { + this.onError(err); + } + + return this.state; + } + } +} \ No newline at end of file diff --git a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts index f641ea29..5045a7f9 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -4,7 +4,6 @@ import type { Hash, Hex, PublicClient, - TransactionReceipt, TypedDataDomain, WalletClient, } from 'viem'; @@ -28,16 +27,31 @@ import { StepType, } from '../../../types'; import { useTransactionStatusModal } from '../../ui/modals/_internal/components/transactionStatusModal'; +import AsyncResultHandler, { Result } from './asyncResultHandler'; -export enum TransactionState { - IDLE = 'IDLE', - SWITCH_CHAIN = 'SWITCH_CHAIN', - CHECKING_STEPS = 'CHECKING_STEPS', - TOKEN_APPROVAL = 'TOKEN_APPROVAL', - TOKEN_APPROVED = 'APPROVED_TOKEN', - EXECUTING_TRANSACTION = 'EXECUTING_TRANSACTION', - SUCCESS = 'SUCCESS', - ERROR = 'ERROR', +export interface TransactionState { + switchChain: { + needed: boolean; + processing: boolean; + processed: boolean; + }; + approval: { + needed: boolean; + processing: boolean; + processed: boolean; + approve: () => any; + }; + steps: { + checking: boolean; + checked: boolean; + steps?: Step[]; + }; + transaction: { + ready: boolean; + executing: boolean; + executed: boolean; + execute: (props: TransactionInput) => any; + }; } export enum TransactionType { @@ -117,49 +131,14 @@ type TransactionInput = props: CancelInput; }; -interface StateConfig { - config: TransactionConfig; - onTransactionSent?: (hash: Hash) => void; - onSuccess?: (hash: Hash) => void; - onError?: (error: Error) => void; -} - -interface TransactionStep { - isReadyToExecute: boolean; - isExecuting: boolean; - executed?: boolean; -} - -interface ApprovalStep { - isReadyToApprove: boolean; - isApproving: boolean; - approved?: boolean; -} - -export interface TransactionSteps { - switchChain: TransactionStep & { - execute: () => Promise; - }; - approval: ApprovalStep & { - approve: () => - | Promise<{ receipt: TransactionReceipt } | undefined> - | Promise - | undefined; - }; - transaction: TransactionStep & { - execute: () => Promise<{ hash: Hash } | undefined> | Promise; - }; -} - const debug = (message: string, data?: any) => { console.debug(`[TransactionMachine] ${message}`, data || ''); }; export class TransactionMachine { - private currentState: TransactionState; + private transactionState: TransactionState; private marketplaceClient: SequenceMarketplace; - private memoizedSteps: TransactionSteps | null = null; - private lastProps: TransactionInput['props'] | null = null; + private resultHandler: AsyncResultHandler; constructor( private readonly config: StateConfig, @@ -168,13 +147,44 @@ export class TransactionMachine { private readonly openSelectPaymentModal: ( settings: SelectPaymentSettings, ) => void, + private readonly accountChainId: number, private readonly switchChainFn: (chainId: string) => Promise, ) { - this.currentState = TransactionState.IDLE; + this.transactionState = { + switchChain: { + needed: false, + processing: false, + processed: false, + }, + approval: { + needed: false, + processing: false, + processed: false, + approve: () => this.approve, + }, + steps: { + checking: false, + checked: false, + }, + transaction: { + ready: false, + executing: false, + executed: false, + execute: (props: TransactionInput) => + this.execute({ props } as { props: TransactionInput }), + }, + }; this.marketplaceClient = getMarketplaceClient( config.config.chainId, config.config.sdkConfig, ); + this.resultHandler = new AsyncResultHandler(config.onError); + } + + private async executeOperation( + operation: () => Promise, + ): Promise> { + return this.resultHandler.execute(operation); } private getAccount() { @@ -195,11 +205,11 @@ export class TransactionMachine { (collection) => collection.collectionAddress.toLowerCase() === collectionAddress.toLowerCase() && - this.getChainId() === Number(collection.chainId), + this.accountChainId === Number(collection.chainId), ); const receiver = - this.getChainId() === avalanche.id + this.accountChainId === avalanche.id ? avalancheAndOptimismPlatformFeeRecipient : defaultPlatformFeeRecipient; @@ -218,101 +228,112 @@ export class TransactionMachine { return this.getAccount().address; } - private async generateSteps({ - type, - props, - }: TransactionInput): Promise { - debug('Generating steps', { type, props }); + private async fetchSteps({ type, props }: TransactionInput) { + debug('Fetching steps', { type, props }); + const { collectionAddress } = this.config.config; const address = this.getAccountAddress(); - switch (type) { - case TransactionType.BUY: - return this.marketplaceClient - .generateBuyTransaction({ - collectionAddress, - buyer: address, - walletType: this.config.config.walletKind, - marketplace: props.marketplace, - ordersData: [ - { - orderId: props.orderId, - quantity: props.quantity || '1', - }, - ], - additionalFees: [this.getMarketplaceFee(collectionAddress)], - }) - .then((resp) => resp.steps); - - case TransactionType.SELL: - return this.marketplaceClient - .generateSellTransaction({ - collectionAddress, - seller: address, - walletType: this.config.config.walletKind, - marketplace: props.marketplace, - ordersData: [ - { - orderId: props.orderId, - quantity: props.quantity || '1', - }, - ], - additionalFees: [], - }) - .then((resp) => resp.steps); - - case TransactionType.LISTING: - return this.marketplaceClient - .generateListingTransaction({ - collectionAddress, - owner: address, - walletType: this.config.config.walletKind, - contractType: props.contractType, - orderbook: OrderbookKind.sequence_marketplace_v2, - listing: props.listing, - }) - .then((resp) => resp.steps); - - case TransactionType.OFFER: - return this.marketplaceClient - .generateOfferTransaction({ - collectionAddress, - maker: address, - walletType: this.config.config.walletKind, - contractType: props.contractType, - orderbook: OrderbookKind.sequence_marketplace_v2, - offer: props.offer, - }) - .then((resp) => resp.steps); - - case TransactionType.CANCEL: - return this.marketplaceClient - .generateCancelTransaction({ - collectionAddress, - maker: address, - marketplace: props.marketplace, - orderId: props.orderId, - }) - .then((resp) => resp.steps); - default: - throw new Error(`Unknown transaction type: ${type}`); + await this.switchChain(); + + this.transition((prevState) => ({ + ...prevState, + steps: { + ...prevState.steps, + checking: true, + checked: false, + }, + })); + + if (type === TransactionType.BUY) { + const generateBuyTransactionSteps = + await this.marketplaceClient.generateBuyTransaction({ + collectionAddress, + buyer: address, + walletType: this.config.config.walletKind, + marketplace: props.marketplace, + ordersData: [ + { + orderId: props.orderId, + quantity: props.quantity || '1', + }, + ], + additionalFees: [this.getMarketplaceFee(collectionAddress)], + }); + + return generateBuyTransactionSteps.steps; } - } - private clearMemoizedSteps() { - debug('Clearing memoized steps'); - this.memoizedSteps = null; - this.lastProps = null; - } + if (type === TransactionType.SELL) { + const generateSellTransactionSteps = + await this.marketplaceClient.generateSellTransaction({ + collectionAddress, + seller: address, + walletType: this.config.config.walletKind, + marketplace: props.marketplace, + ordersData: [ + { + orderId: props.orderId, + quantity: props.quantity || '1', + }, + ], + additionalFees: [], + }); + + return generateSellTransactionSteps.steps; + } + + if (type === TransactionType.LISTING) { + const generateListingTransactionSteps = + await this.marketplaceClient.generateListingTransaction({ + collectionAddress, + owner: address, + walletType: this.config.config.walletKind, + contractType: props.contractType, + orderbook: OrderbookKind.sequence_marketplace_v2, + listing: props.listing, + }); - private async transition(newState: TransactionState) { - debug(`State transition: ${this.currentState} -> ${newState}`); - this.currentState = newState; - this.clearMemoizedSteps(); + return generateListingTransactionSteps.steps; + } + + if (type === TransactionType.OFFER) { + const generateOfferTransactionSteps = + await this.marketplaceClient.generateOfferTransaction({ + collectionAddress, + maker: address, + walletType: this.config.config.walletKind, + contractType: props.contractType, + orderbook: OrderbookKind.sequence_marketplace_v2, + offer: props.offer, + }); + + return generateOfferTransactionSteps.steps; + } + + if (type === TransactionType.CANCEL) { + const generateCancelTransactionSteps = + await this.marketplaceClient.generateCancelTransaction({ + collectionAddress, + maker: address, + marketplace: props.marketplace, + orderId: props.orderId, + }); + + return generateCancelTransactionSteps.steps; + } + + throw new Error('Invalid transaction type'); } - private getChainId() { - return this.walletClient.chain?.id; + private async transition( + stateUpdater: (prevState: TransactionState) => TransactionState, + ) { + const newState = stateUpdater(this.transactionState); + debug( + `State transition: ${JSON.stringify(this.transactionState)} -> ${JSON.stringify(newState)}`, + ); + this.transactionState = newState; } private getChainForTransaction() { @@ -323,48 +344,50 @@ export class TransactionMachine { } private isOnCorrectChain() { - return this.getChainId() === Number(this.config.config.chainId); + return this.accountChainId === Number(this.config.config.chainId); } - private async switchChain(): Promise { + private async switchChain() { debug('Checking chain', { - currentChain: this.getChainId(), + currentChain: this.accountChainId, targetChain: Number(this.config.config.chainId), }); - if (!this.isOnCorrectChain()) { - await this.transition(TransactionState.SWITCH_CHAIN); + if (this.isOnCorrectChain()) return; + + await this.transition((prevState) => ({ + ...prevState, + switchChain: { + ...prevState.switchChain, + needed: true, + processing: true, + }, + })); + + try { await this.switchChainFn(this.config.config.chainId); + await this.walletClient.switchChain({ id: Number(this.config.config.chainId), }); debug('Switched chain'); - } - } - - async start({ props }: { props: TransactionInput['props'] }) { - debug('Starting transaction', props); - try { - await this.transition(TransactionState.CHECKING_STEPS); - const { type } = this.config.config; - - const steps = await this.generateSteps({ - type, - props, - } as TransactionInput); - - for (const step of steps) { - try { - await this.executeStep({ step, props }); - } catch (error) { - await this.transition(TransactionState.ERROR); - throw error; - } - } - await this.transition(TransactionState.SUCCESS); + await this.transition((prevState) => ({ + ...prevState, + switchChain: { + needed: false, + processing: false, + processed: true, + }, + })); } catch (error) { - debug('Transaction failed', error); - await this.transition(TransactionState.ERROR); + await this.transition((prevState) => ({ + ...prevState, + switchChain: { + needed: true, + processing: false, + processed: false, + }, + })); throw error; } } @@ -372,7 +395,15 @@ export class TransactionMachine { private async handleTransactionSuccess(hash?: Hash) { if (!hash) { // TODO: This is to handle signature steps, but it's not ideal - await this.transition(TransactionState.SUCCESS); + await this.transition((prevState) => ({ + ...prevState, + transaction: { + ...prevState.transaction, + ready: false, + executing: false, + executed: true, + }, + })); return; } @@ -381,7 +412,15 @@ export class TransactionMachine { const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); debug('Transaction confirmed', receipt); - await this.transition(TransactionState.SUCCESS); + await this.transition((prevState) => ({ + ...prevState, + transaction: { + ...prevState.transaction, + ready: false, + executing: false, + executed: true, + }, + })); this.config.onSuccess?.(hash); } @@ -401,10 +440,11 @@ export class TransactionMachine { value: BigInt(step.value || '0'), }; debug('Executing transaction', transactionData); + const hash = await this.walletClient.sendTransaction(transactionData); useTransactionStatusModal().show({ - chainId: this.getChainId()! as unknown as string, + chainId: String(this.accountChainId), collectionAddress: this.config.config.collectionAddress as Hex, collectibleId: this.config.config.collectibleId as string, hash: hash as Hash, @@ -418,7 +458,10 @@ export class TransactionMachine { debug('Transaction submitted', { hash }); - await this.handleTransactionSuccess(hash); + if (!this.transactionState.approval.processing) { + await this.handleTransactionSuccess(hash); + } + return hash; } @@ -484,7 +527,15 @@ export class TransactionMachine { step: Step; props: BuyInput; }) { - this.transition(TransactionState.EXECUTING_TRANSACTION); + this.transition((prevState) => ({ + ...prevState, + transaction: { + ...prevState.transaction, + ready: false, + executing: true, + executed: false, + }, + })); const [checkoutOptions, orders] = await Promise.all([ this.marketplaceClient.checkoutOptionsMarketplace({ wallet: this.getAccountAddress(), @@ -517,7 +568,7 @@ export class TransactionMachine { } const paymentModalProps = { - chain: this.getChainId()!, + chain: this.accountChainId, collectibles: [ { tokenId: order.tokenId, @@ -543,139 +594,169 @@ export class TransactionMachine { await this.openPaymentModalWithPromise(paymentModalProps); } - private async executeStep({ - step, + private async execute({ props, }: { - step: Step; - props: TransactionInput['props']; - }) { - console.log('step:', step); - debug('Executing step', { stepId: step.id }); - if (!step.to && !step.signature) { + props: TransactionInput; + }): Promise> { + const { steps } = this.transactionState; + + debug('Executing transaction', { props, steps }); + + if (!steps.steps) { + throw new Error('No steps found'); + } + + const executionStep = steps.steps[0]; + debug('Executing step', { stepId: executionStep.id }); + + if (!executionStep.to && !executionStep.signature) { throw new Error('Invalid step data'); } - try { - await this.switchChain(); - if (step.id === StepType.buy) { - await this.executeBuyStep({ step, props: props as BuyInput }); - } else if (step.signature) { - await this.executeSignature(step); - } else if (step.id === StepType.tokenApproval) { - //TODO: Add some sort ofs callback heres - const hash = await this.executeTransaction({ - step, - isTokenApproval: true, + this.transition((prevState) => ({ + ...prevState, + transaction: { + ...prevState.transaction, + ready: true, + executing: true, + executed: false, + }, + })); + + return this.executeOperation(async () => { + if (executionStep.id === StepType.buy) { + await this.executeBuyStep({ + step: executionStep, + props: props.props as BuyInput, }); - return { hash }; + } else if (executionStep.signature) { + await this.executeSignature(executionStep); } else { - const hash = await this.executeTransaction({ - step, + await this.executeTransaction({ + step: executionStep, isTokenApproval: false, }); - return { hash }; } - } catch (error) { - this.config.onError?.(error as Error); - throw error; - } + + return { + ...this.transactionState, + transaction: { + ...this.transactionState.transaction, + ready: false, + executing: false, + executed: true, + }, + }; + }); } private async approve({ step, - }: { step: Step }): Promise<{ receipt: TransactionReceipt }> { - return new Promise(async (resolve, reject) => { - try { - await this.transition(TransactionState.TOKEN_APPROVAL); - - debug('Executing step', { stepId: step.id }); - if (!step.to && !step.signature) { - throw new Error('Invalid step data'); - } + }: { + step: Step; + }): Promise> { + return this.executeOperation(async () => { + await this.transition((prevState) => ({ + ...prevState, + approval: { + ...prevState.approval, + needed: true, + processing: true, + processed: false, + }, + })); - if (step.id !== StepType.tokenApproval) { - throw new Error('Invalid approval step'); - } + debug('Executing step', { stepId: step.id }); - await this.switchChain(); + if (!step.to && !step.signature) { + throw new Error('Invalid step data'); + } - const hash = await this.executeTransaction({ - step, - isTokenApproval: true, - }); + if (step.id !== StepType.tokenApproval) { + throw new Error('Invalid approval step'); + } - const receipt = await this.publicClient.waitForTransactionReceipt({ - hash, - }); + const hash = await this.executeTransaction({ + step, + isTokenApproval: true, + }); + + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + }); - debug('Approval confirmed', receipt); - await this.transition(TransactionState.TOKEN_APPROVED); + debug('Approval confirmed', receipt); - resolve({ receipt }); - } catch (error) { - this.config.onError?.(error as Error); - reject(error); - } + await this.transition((prevState) => ({ + ...prevState, + approval: { + ...prevState.approval, + needed: false, + processing: false, + processed: true, + }, + })); + + return { + ...this.transactionState, + approval: { + ...this.transactionState.approval, + needed: false, + processing: false, + processed: true, + }, + }; }); } - async getTransactionSteps( - props: TransactionInput['props'], - ): Promise { + async refreshStepsGetState(props: TransactionInput['props']) { debug('Getting transaction steps', props); - // Return memoized value if props and state haven't changed - if ( - this.memoizedSteps && - this.lastProps && - JSON.stringify(props) === JSON.stringify(this.lastProps) - ) { - debug('Returning memoized steps'); - return this.memoizedSteps; - } - const type = this.config.config.type; - const steps = await this.generateSteps({ - type, - props, - } as TransactionInput); - // Extract execution step, it should always be the last step + const type = this.config.config.type as TransactionInput['type']; + const steps = await this.fetchSteps({ type, props } as TransactionInput); const executionStep = steps.pop(); + if (!executionStep) { throw new Error('No steps found'); } if (executionStep.id === StepType.tokenApproval) { throw new Error('No execution step found, only approval step'); } + const approvalStep = steps.pop(); if (steps.length > 0) { throw new Error('Unexpected steps found'); } - this.lastProps = props; - this.memoizedSteps = { + if (approvalStep) { + await this.approve({ step: approvalStep }); + } + + return { switchChain: { - isReadyToExecute: !this.isOnCorrectChain(), - isExecuting: this.currentState === TransactionState.SWITCH_CHAIN, - execute: () => this.switchChain(), + needed: !this.isOnCorrectChain(), + processing: false, + processed: false, }, approval: { - isReadyToApprove: Boolean(approvalStep), - approve: () => approvalStep && this.approve({ step: approvalStep }), - isApproving: this.currentState === TransactionState.TOKEN_APPROVAL, - approved: this.currentState === TransactionState.TOKEN_APPROVED, + ...this.transactionState.approval, + needed: !!approvalStep, + processing: false, + processed: false, + }, + steps: { + checking: false, + checked: true, + steps: steps, }, transaction: { - isReadyToExecute: Boolean(executionStep), - execute: () => this.executeStep({ step: executionStep, props }), - isExecuting: - this.currentState === TransactionState.EXECUTING_TRANSACTION, - executed: this.currentState === TransactionState.SUCCESS, + ...this.transactionState.transaction, + ready: true, + executing: false, + executed: false, }, - } as const; - - debug('Generated new transaction steps', this.memoizedSteps); - return this.memoizedSteps; + }; } } diff --git a/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts b/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts index a2e4bd46..d2ae0782 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts @@ -2,7 +2,10 @@ import { useSelectPaymentModal } from '@0xsequence/kit-checkout'; import type { Hash } from 'viem'; import { useAccount, useSwitchChain, useWalletClient } from 'wagmi'; import { getPublicRpcClient } from '../../../utils'; -import { useConfig, useMarketplaceConfig } from '../../hooks'; +import { + useConfig, + useMarketplaceConfig, +} from '../../hooks'; import { useSwitchChainModal } from '../../ui/modals/_internal/components/switchChainModal'; import { WalletKind } from '../api'; import { @@ -19,6 +22,7 @@ export const useTransactionMachine = ( config: UseTransactionMachineConfig, onSuccess?: (hash: Hash) => void, onError?: (error: Error) => void, + closeActionModal?: () => void, onTransactionSent?: (hash: Hash) => void, ) => { const { data: walletClient } = useWalletClient(); @@ -29,7 +33,7 @@ export const useTransactionMachine = ( const { openSelectPaymentModal } = useSelectPaymentModal(); const { chains } = useSwitchChain(); - const { connector } = useAccount(); + const { connector, chainId: accountChainId } = useAccount(); const walletKind = connector?.id === 'sequence' ? WalletKind.sequence : WalletKind.unknown; @@ -37,7 +41,7 @@ export const useTransactionMachine = ( throw marketplaceError; //TODO: Add error handling } - if (!walletClient || !marketplaceConfig) return null; + if (!walletClient || !marketplaceConfig || !accountChainId) return null; return new TransactionMachine( { @@ -49,6 +53,7 @@ export const useTransactionMachine = ( walletClient, getPublicRpcClient(config.chainId), openSelectPaymentModal, + accountChainId, async (chainId) => { return new Promise((resolve, reject) => { showSwitchChainModal({ @@ -57,8 +62,13 @@ export const useTransactionMachine = ( resolve(); }, onError: (error) => { + console.log('Switch chain error', error); reject(error); }, + onClose: () => { + closeActionModal?.(); + reject(new Error('Switch chain modal closed')); + }, }); }); }, From fe1db9668efc854fb4e6c0e664cba54ee102ce97 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Mon, 9 Dec 2024 13:58:35 +0300 Subject: [PATCH 26/74] remove unused hooks and refactor useCancelOrder temporarily --- packages/sdk/src/react/hooks/index.ts | 3 +- .../sdk/src/react/hooks/useBuyCollectable.tsx | 38 ----------- .../sdk/src/react/hooks/useCancelOrder.tsx | 45 +++++++++++-- .../sdk/src/react/hooks/useCreateListing.tsx | 65 ------------------- packages/sdk/src/react/hooks/useMakeOffer.tsx | 62 ------------------ packages/sdk/src/react/hooks/useSell.tsx | 62 ------------------ 6 files changed, 41 insertions(+), 234 deletions(-) delete mode 100644 packages/sdk/src/react/hooks/useBuyCollectable.tsx delete mode 100644 packages/sdk/src/react/hooks/useCreateListing.tsx delete mode 100644 packages/sdk/src/react/hooks/useMakeOffer.tsx delete mode 100644 packages/sdk/src/react/hooks/useSell.tsx diff --git a/packages/sdk/src/react/hooks/index.ts b/packages/sdk/src/react/hooks/index.ts index 7fdfebb0..6e4b7e88 100644 --- a/packages/sdk/src/react/hooks/index.ts +++ b/packages/sdk/src/react/hooks/index.ts @@ -25,5 +25,4 @@ export * from './useTransferTokens'; export * from './useCheckoutOptions'; export * from './useListCollections'; export * from './useGenerateBuyTransaction'; -export * from './useCancelOrder'; -export * from './useBuyCollectable'; +export * from './useCancelOrder'; \ No newline at end of file diff --git a/packages/sdk/src/react/hooks/useBuyCollectable.tsx b/packages/sdk/src/react/hooks/useBuyCollectable.tsx deleted file mode 100644 index 9fce58b4..00000000 --- a/packages/sdk/src/react/hooks/useBuyCollectable.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - type BuyInput, - TransactionType, -} from '../_internal/transaction-machine/execute-transaction'; -import { - useTransactionMachine, - type UseTransactionMachineConfig, -} from '../_internal/transaction-machine/useTransactionMachine'; - -interface UseBuyOrderArgs extends Omit { - onSuccess?: (hash: string) => void; - onError?: (error: Error) => void; - onTransactionSent?: (hash: string) => void; -} - -export const useBuyCollectable = ({ - onSuccess, - onError, - onTransactionSent, - ...config -}: UseBuyOrderArgs) => { - const machine = useTransactionMachine( - { - ...config, - type: TransactionType.BUY, - }, - onSuccess, - onError, - onTransactionSent, - ); - - return { - buy: (props: BuyInput) => machine?.start({ props }), - onError, - onSuccess, - onTransactionSent, - }; -}; diff --git a/packages/sdk/src/react/hooks/useCancelOrder.tsx b/packages/sdk/src/react/hooks/useCancelOrder.tsx index 19112377..e8220fda 100644 --- a/packages/sdk/src/react/hooks/useCancelOrder.tsx +++ b/packages/sdk/src/react/hooks/useCancelOrder.tsx @@ -1,9 +1,10 @@ +import { useState } from 'react'; import { type CancelInput, + TransactionState, TransactionType, } from '../_internal/transaction-machine/execute-transaction'; import { - useTransactionMachine, type UseTransactionMachineConfig, } from '../_internal/transaction-machine/useTransactionMachine'; @@ -17,20 +18,54 @@ export const useCancelOrder = ({ onSuccess, onError, onTransactionSent, - ...config }: UseCancelOrderArgs) => { - const machine = useTransactionMachine( +// const [isLoading, setIsLoading] = useState(true); + const [transactionState] = useState(null); + /*const machine = useTransactionMachine( { ...config, type: TransactionType.CANCEL, }, onSuccess, onError, + undefined, onTransactionSent, - ); + );*/ + + /** + useEffect(() => { + if (!machine || transactionState?.steps.checked) return; + + machine + .refreshStepsGetState({ + orderId: config.orderId, + marketplace: config.marketplace, + }) + .then((state) => { + if (!state.steps) return; + + setTransactionState(state); + setIsLoading(false); + }) + .catch((error) => { + console.error('Error loading make offer steps', error); + setIsLoading(false); + }); + }, [currency, machine, order]); + */ + + const handleStepExecution = async (props: CancelInput) => { + await transactionState?.transaction.execute({ + type: TransactionType.CANCEL, + props: { + orderId: props.orderId, + marketplace: props.marketplace + }, + }); + }; return { - cancel: (props: CancelInput) => machine?.start({ props }), + cancel: (props: CancelInput) => handleStepExecution(props), onError, onSuccess, onTransactionSent, diff --git a/packages/sdk/src/react/hooks/useCreateListing.tsx b/packages/sdk/src/react/hooks/useCreateListing.tsx deleted file mode 100644 index c2838357..00000000 --- a/packages/sdk/src/react/hooks/useCreateListing.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { Hash } from 'viem'; -import { - type ListingInput, - TransactionType, - type TransactionSteps, -} from '../_internal/transaction-machine/execute-transaction'; -import { - useTransactionMachine, - type UseTransactionMachineConfig, -} from '../_internal/transaction-machine/useTransactionMachine'; - -interface UseCreateListingArgs - extends Omit { - onSuccess?: (hash: Hash) => void; - onError?: (error: Error) => void; - onTransactionSent?: (hash: Hash) => void; -} - -export const useCreateListing = ({ - onSuccess, - onError, - onTransactionSent, - ...config -}: UseCreateListingArgs) => { - const [isLoading, setIsLoading] = useState(false); - const [steps, setSteps] = useState(null); - - const machine = useTransactionMachine( - { - ...config, - type: TransactionType.LISTING, - }, - onSuccess, - onError, - onTransactionSent, - ); - - const loadSteps = useCallback( - async (props: ListingInput) => { - if (!machine) return; - setIsLoading(true); - try { - const generatedSteps = await machine.getTransactionSteps(props); - setSteps(generatedSteps); - } catch (error) { - onError?.(error as Error); - } finally { - setIsLoading(false); - } - }, - [machine, onError], - ); - - return { - createListing: (props: ListingInput) => machine?.start({ props }), - getListingSteps: (props: ListingInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), - onError, - onSuccess, - }; -}; diff --git a/packages/sdk/src/react/hooks/useMakeOffer.tsx b/packages/sdk/src/react/hooks/useMakeOffer.tsx deleted file mode 100644 index c88f5152..00000000 --- a/packages/sdk/src/react/hooks/useMakeOffer.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { Hash } from 'viem'; -import { - type OfferInput, - TransactionType, - type TransactionSteps, -} from '../_internal/transaction-machine/execute-transaction'; -import { - useTransactionMachine, - type UseTransactionMachineConfig, -} from '../_internal/transaction-machine/useTransactionMachine'; - -interface UseMakeOfferArgs extends Omit { - onSuccess?: (hash: Hash) => void; - onError?: (error: Error) => void; - onTransactionSent?: (hash: Hash) => void; -} - -export const useMakeOffer = ({ - onSuccess, - onError, - onTransactionSent, - ...config -}: UseMakeOfferArgs) => { - const [isLoading, setIsLoading] = useState(false); - const [steps, setSteps] = useState(null); - - const machine = useTransactionMachine( - { - ...config, - type: TransactionType.OFFER, - }, - onSuccess, - onError, - onTransactionSent, - ); - - const loadSteps = useCallback( - async (props: OfferInput) => { - if (!machine) return; - setIsLoading(true); - try { - const generatedSteps = await machine.getTransactionSteps(props); - setSteps(generatedSteps); - } catch (error) { - onError?.(error as Error); - } finally { - setIsLoading(false); - } - }, - [machine, onError], - ); - - return { - makeOffer: (props: OfferInput) => machine?.start({ props }), - getMakeOfferSteps: (props: OfferInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), - }; -}; diff --git a/packages/sdk/src/react/hooks/useSell.tsx b/packages/sdk/src/react/hooks/useSell.tsx deleted file mode 100644 index 8f7672f4..00000000 --- a/packages/sdk/src/react/hooks/useSell.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { Hash } from 'viem'; -import { - type SellInput, - TransactionType, - type TransactionSteps, -} from '../_internal/transaction-machine/execute-transaction'; -import { - useTransactionMachine, - type UseTransactionMachineConfig, -} from '../_internal/transaction-machine/useTransactionMachine'; - -interface UseSellArgs extends Omit { - onSuccess?: (hash: Hash) => void; - onError?: (error: Error) => void; - onTransactionSent?: (hash: Hash) => void; -} - -export const useSell = ({ - onSuccess, - onError, - onTransactionSent, - ...config -}: UseSellArgs) => { - const [isLoading, setIsLoading] = useState(false); - const [steps, setSteps] = useState(null); - - const machine = useTransactionMachine( - { - ...config, - type: TransactionType.SELL, - }, - onSuccess, - onError, - onTransactionSent, - ); - - const loadSteps = useCallback( - async (props: SellInput) => { - if (!machine) return; - setIsLoading(true); - try { - const generatedSteps = await machine.getTransactionSteps(props); - setSteps(generatedSteps); - } catch (error) { - onError?.(error as Error); - } finally { - setIsLoading(false); - } - }, - [machine, onError], - ); - - return { - sell: (props: SellInput) => machine?.start({ props }), - getSellSteps: (props: SellInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), - }; -}; From a8b6275ad322c95c3ce6d0d48c46c3a034fc0205 Mon Sep 17 00:00:00 2001 From: Mithat Akbulut Date: Mon, 9 Dec 2024 13:58:48 +0300 Subject: [PATCH 27/74] update ActionModal to support async onClick handlers --- .../modals/_internal/components/actionModal/ActionModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx b/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx index 78ae9eda..4e22fac9 100644 --- a/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx +++ b/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx @@ -30,7 +30,7 @@ export interface ActionModalProps { children: React.ReactNode; ctas: { label: string; - onClick: () => void; + onClick: (() => Promise) | (() => void); pending?: boolean; disabled?: boolean; hidden?: boolean; @@ -73,7 +73,9 @@ export const ActionModal = observer(