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..ddf6764b 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/execute-transaction.ts @@ -26,34 +26,51 @@ import { type Step, StepType, } from '../../../types'; +import { ShowTransactionStatusModalArgs } from '../../ui/modals/_internal/components/transactionStatusModal'; -export enum TransactionState { - IDLE = 'IDLE', - SWITCH_CHAIN = 'SWITCH_CHAIN', - CHECKING_STEPS = 'CHECKING_STEPS', - TOKEN_APPROVAL = 'TOKEN_APPROVAL', - EXECUTING_TRANSACTION = 'EXECUTING_TRANSACTION', - CONFIRMING = 'CONFIRMING', - SUCCESS = 'SUCCESS', - ERROR = 'ERROR', -} +export type TransactionState = { + switchChain: { + needed: boolean; + processing: boolean; + processed: boolean; + }; + approval: { + checked: boolean; + needed: boolean; + processing: boolean; + processed: boolean; + }; + steps: { + checking: boolean; + checked: boolean; + steps?: Step[]; + }; + transaction: { + ready: boolean; + executing: boolean; + executed: boolean; + }; +} | null; export enum TransactionType { BUY = 'BUY', SELL = 'SELL', LISTING = 'LISTING', + TRANSFER = 'TRANSFER', OFFER = 'OFFER', CANCEL = 'CANCEL', } export interface TransactionConfig { - type: TransactionType; + transactionInput: TransactionInput; walletKind: WalletKind; chainId: string; chains: readonly Chain[]; collectionAddress: string; + collectibleId?: string; sdkConfig: SdkConfig; marketplaceConfig: MarketplaceConfig; + fetchStepsOnInitialize?: boolean; } interface StateConfig { @@ -113,42 +130,19 @@ type TransactionInput = props: CancelInput; }; -interface StateConfig { - config: TransactionConfig; - onTransactionSent?: (hash: Hash) => void; - onSuccess?: (hash: Hash) => void; - onError?: (error: Error) => void; -} - -interface TransactionStep { - isPending: boolean; - isExecuting: boolean; -} - -export interface TransactionSteps { - switchChain: TransactionStep & { - execute: () => Promise; - }; - approval: TransactionStep & { - execute: () => - | Promise<{ hash: Hash } | 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; + transactionState: TransactionState; + setTransactionState: React.Dispatch>; + closeActionModal: (() => void) | undefined; + showTransactionStatusModal: ({ + hash, + blocked, + }: Pick) => void; private marketplaceClient: SequenceMarketplace; - private memoizedSteps: TransactionSteps | null = null; - private lastProps: TransactionInput['props'] | null = null; constructor( private readonly config: StateConfig, @@ -157,13 +151,74 @@ export class TransactionMachine { private readonly openSelectPaymentModal: ( settings: SelectPaymentSettings, ) => void, + private readonly accountChainId: number, private readonly switchChainFn: (chainId: string) => Promise, + transactionState: TransactionState, + setTransactionState: React.Dispatch>, + closeActionModal: () => void, + showTransactionStatusModal: ({ + hash, + blocked, + }: Pick) => void, ) { - this.currentState = TransactionState.IDLE; this.marketplaceClient = getMarketplaceClient( config.config.chainId, config.config.sdkConfig, ); + this.transactionState = transactionState; + this.setTransactionState = setTransactionState; + this.closeActionModal = closeActionModal; + this.showTransactionStatusModal = showTransactionStatusModal; + + this.initialize(); + this.watchSwitchChain(); + } + + private async initialize() { + if (this.transactionState || !this.config.config.transactionInput) return; + + debug( + 'Initializing transaction state for', + this.config.config.transactionInput.type, + ); + + const initialState = { + switchChain: { + needed: false, + processing: false, + processed: false, + }, + approval: { + checked: false, + needed: false, + processing: false, + processed: false, + }, + steps: { + checking: false, + checked: false, + steps: undefined, + }, + transaction: { + ready: false, + executing: false, + executed: false, + }, + } as TransactionState; + + this.updateTransactionState(initialState); + + if (this.config.config.fetchStepsOnInitialize) { + await this.fetchSteps(this.config.config.transactionInput); + } + + debug('Watching chain switch'); + debug('Transaction state initialized', this.transactionState); + } + + private updateTransactionState(newState: TransactionState) { + this.setTransactionState(newState); + this.transactionState = newState; } private getAccount() { @@ -184,11 +239,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; @@ -207,101 +262,159 @@ export class TransactionMachine { return this.getAccount().address; } - private async generateSteps({ - type, - props, - }: TransactionInput): Promise { - debug('Generating 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); + async fetchSteps({ type, props }: TransactionInput): Promise { + try { + debug('Fetching steps', { type, props }); - default: - throw new Error(`Unknown transaction type: ${type}`); + const { collectionAddress } = this.config.config; + const address = this.getAccountAddress(); + + if (!this.transactionState) { + throw new Error('Transaction state not found'); + } + + this.setTransactionState((prev) => ({ + ...prev!, + steps: { + ...prev!.steps, + checking: true, + checked: false, + }, + })); + + let steps; + + switch (type) { + case TransactionType.BUY: + steps = 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)], + }) + .then((result) => result.steps); + break; + + case TransactionType.SELL: + steps = await this.marketplaceClient + .generateSellTransaction({ + collectionAddress, + seller: address, + walletType: this.config.config.walletKind, + marketplace: props.marketplace, + ordersData: [ + { + orderId: props.orderId, + quantity: props.quantity || '1', + }, + ], + additionalFees: [], + }) + .then((result) => result.steps); + break; + + case TransactionType.LISTING: + // for fetching steps for creating listing, placeholder pricePerToken is used to not block making request to check if moving and transferring access is approved. we don't need to check if spending erc20 token is approved + const listing = !this.transactionState.approval.checked + ? ({ + ...props.listing, + pricePerToken: '1', + } as CreateReq) + : props.listing; + + steps = await this.marketplaceClient + .generateListingTransaction({ + collectionAddress, + owner: address, + walletType: this.config.config.walletKind, + contractType: props.contractType, + orderbook: OrderbookKind.sequence_marketplace_v2, + listing, + }) + .then((result) => result.steps); + break; + + case TransactionType.OFFER: + steps = await this.marketplaceClient + .generateOfferTransaction({ + collectionAddress, + maker: address, + contractType: props.contractType, + orderbook: OrderbookKind.sequence_marketplace_v2, + offer: props.offer, + }) + .then((result) => result.steps); + break; + + case TransactionType.CANCEL: + steps = await this.marketplaceClient + .generateCancelTransaction({ + collectionAddress, + maker: address, + marketplace: props.marketplace, + orderId: props.orderId, + }) + .then((result) => result.steps); + break; + + default: + throw new Error('Invalid transaction type'); + } + + this.setSteps(steps); + + return steps; + } catch (error) { + this.setTransactionState((prev) => ({ + ...prev!, + steps: { + ...prev!.steps, + checking: false, + checked: false, + steps: undefined, + }, + })); + + this.config.onError?.(error as Error); + + throw error; } } - private clearMemoizedSteps() { - debug('Clearing memoized steps'); - this.memoizedSteps = null; - this.lastProps = null; - } + private setSteps(steps: Step[]) { + debug('Setting steps', steps); - private async transition(newState: TransactionState) { - debug(`State transition: ${this.currentState} -> ${newState}`); - this.currentState = newState; - this.clearMemoizedSteps(); - } + const newState = { + ...this.transactionState!, + approval: { + ...this.transactionState!.approval, + checked: true, + needed: steps.some((step) => step.id === StepType.tokenApproval), + processing: false, + processed: false, + }, + transaction: { + ready: + !this.transactionState?.switchChain.needed && + !this.transactionState?.approval.needed, + }, + steps: { + ...this.transactionState!.steps, + checking: false, + checked: true, + steps: [...steps], + }, + } as TransactionState; - private getChainId() { - return this.walletClient.chain?.id; + this.updateTransactionState(newState); } private getChainForTransaction() { @@ -312,69 +425,212 @@ 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); + + try { await this.switchChainFn(this.config.config.chainId); + await this.walletClient.switchChain({ id: Number(this.config.config.chainId), }); - debug('Switched chain'); + + debug('Switched chain to', this.config.config.chainId); + + this.setTransactionState({ + ...this.transactionState!, + switchChain: { + processed: true, + processing: false, + needed: false, + }, + }); + } catch (error) { + this.setTransactionState({ + ...this.transactionState!, + switchChain: { + processed: false, + processing: false, + needed: true, + }, + }); + + throw error; } } - async start({ props }: { props: TransactionInput['props'] }) { - debug('Starting transaction', props); + private watchSwitchChain() { + let currentState = this.transactionState; + + if (!currentState) return; + + if (!this.isOnCorrectChain()) { + this.switchChain(); + } + } + + approve = async ({ approvalStep }: { approvalStep: Step }): Promise => { + if (!this.transactionState) { + throw new Error('Transaction state not found'); + } + + if (!approvalStep) { + throw new Error('Approval step not found'); + } + + this.setTransactionState((prev) => ({ + ...prev!, + approval: { + ...prev!.approval, + needed: true, + processing: true, + processed: false, + }, + })); + 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; - } + this.setTransactionState({ + ...this.transactionState!, + approval: { + ...this.transactionState!.approval, + needed: true, + processing: true, + processed: false, + }, + }); + + debug('Executing step', { stepId: approvalStep.id }); + + if (!approvalStep.to && !approvalStep.signature) { + throw new Error('Invalid step data'); + } + + if (approvalStep.id !== StepType.tokenApproval) { + throw new Error('Invalid approval step'); } - await this.transition(TransactionState.SUCCESS); + const hash = await this.executeTransaction({ + step: approvalStep, + isTokenApproval: true, + }); + + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + }); + + debug('Approval confirmed', receipt); + + this.updateTransactionState({ + ...this.transactionState!, + approval: { + ...this.transactionState!.approval, + needed: false, + processing: false, + processed: true, + }, + }); } catch (error) { - debug('Transaction failed', error); - await this.transition(TransactionState.ERROR); + this.setTransactionState({ + ...this.transactionState!, + approval: { + checked: true, + needed: true, + processing: false, + processed: false, + }, + }); throw error; } - } + }; - private async handleTransactionSuccess(hash?: Hash) { - if (!hash) { - // TODO: This is to handle signature steps, but it's not ideal - await this.transition(TransactionState.SUCCESS); - return; + async execute(transactionInput: TransactionInput): Promise { + if (!this.transactionState) throw new Error('Transaction state not found'); + if (this.transactionState.approval.needed) + throw new Error('Approval needed before executing transaction'); + + const steps = await this.fetchSteps(transactionInput); + const transactionInputTypeToStepTypeMap = { + [TransactionType.BUY]: StepType.buy, + [TransactionType.SELL]: StepType.sell, + [TransactionType.LISTING]: StepType.createListing, + [TransactionType.OFFER]: StepType.createOffer, + [TransactionType.CANCEL]: StepType.cancel, + }; + const executionStep = steps.find( + (step) => + step.id === transactionInputTypeToStepTypeMap[transactionInput.type], + ); + const approvalStep = steps.find( + (step) => step.id === StepType.tokenApproval, + ); + + if (approvalStep) { + throw new Error('Approval needed before executing transaction'); } - await this.transition(TransactionState.CONFIRMING); - this.config.onTransactionSent?.(hash); - const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); - debug('Transaction confirmed', receipt); + debug('Executing transaction', { props: transactionInput, executionStep }); - await this.transition(TransactionState.SUCCESS); - this.config.onSuccess?.(hash); + if (!executionStep) { + throw new Error('No execution step found'); + } + + if (!executionStep.to && !executionStep.signature) { + throw new Error('Invalid step data'); + } + + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + ready: true, + executing: true, + executed: false, + }, + }); + + try { + if (executionStep.id === StepType.buy) { + await this.executeBuyStep({ + step: executionStep, + props: transactionInput.props as BuyInput, + }); + } else if (executionStep.signature) { + await this.executeSignature(executionStep); + } else { + await this.executeTransaction({ + step: executionStep, + isTokenApproval: false, + }); + } + + this.kill(); + } catch (error) { + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + ready: true, + executing: false, + executed: false, + }, + }); + throw error; + } } - private async executeTransaction(step: Step): Promise { + private async executeTransaction({ + step, + isTokenApproval, + }: { + step: Step; + isTokenApproval: boolean; + }): Promise { const transactionData = { account: this.getAccount(), chain: this.getChainForTransaction(), @@ -383,10 +639,38 @@ export class TransactionMachine { value: BigInt(step.value || '0'), }; debug('Executing transaction', transactionData); - const hash = await this.walletClient.sendTransaction(transactionData); - debug('Transaction submitted', { hash }); - await this.handleTransactionSuccess(hash); - return hash; + + try { + const hash = await this.walletClient.sendTransaction(transactionData); + + this.showTransactionStatusModal({ hash, blocked: isTokenApproval }); + + if (!isTokenApproval) { + this.closeActionModal && this.closeActionModal(); + } + + debug('Transaction submitted', { hash }); + + if (!isTokenApproval) { + await this.handleTransactionSuccess(hash); + } + + return hash; + } catch (error) { + this.config.onError?.(error as Error); + + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + ready: true, + executing: false, + executed: false, + }, + }); + + throw error; + } } private async executeSignature(step: Step) { @@ -423,6 +707,17 @@ export class TransactionMachine { executeType: ExecuteType.order, body: step.post, }); + + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + ready: false, + executing: false, + executed: true, + }, + }); + await this.handleTransactionSuccess(); } @@ -433,6 +728,15 @@ export class TransactionMachine { this.openSelectPaymentModal({ ...settings, onSuccess: async (hash: string) => { + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + executing: false, + executed: true, + }, + }); + await this.handleTransactionSuccess(hash as Hash); resolve(); }, @@ -451,7 +755,6 @@ export class TransactionMachine { step: Step; props: BuyInput; }) { - this.transition(TransactionState.EXECUTING_TRANSACTION); const [checkoutOptions, orders] = await Promise.all([ this.marketplaceClient.checkoutOptionsMarketplace({ wallet: this.getAccountAddress(), @@ -484,7 +787,7 @@ export class TransactionMachine { } const paymentModalProps = { - chain: this.getChainId()!, + chain: this.accountChainId, collectibles: [ { tokenId: order.tokenId, @@ -510,94 +813,41 @@ export class TransactionMachine { await this.openPaymentModalWithPromise(paymentModalProps); } - private async executeStep({ - step, - props, - }: { - step: Step; - props: TransactionInput['props']; - }) { - debug('Executing step', { stepId: step.id }); - if (!step.to && !step.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); - return { hash } - } else { - const hash = await this.executeTransaction(step); - this.config.onSuccess?.(hash); - return { hash }; - } - } catch (error) { - this.config.onError?.(error as Error); - throw error; - } - } - - async getTransactionSteps( - props: TransactionInput['props'], - ): Promise { - 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; + private async handleTransactionSuccess(hash?: Hash) { + if (!hash) { + // TODO: This is to handle signature steps, but it's not ideal + this.setTransactionState({ + ...this.transactionState!, + transaction: { + ...this.transactionState!.transaction, + ready: false, + executing: false, + executed: true, + }, + }); + return; } - 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 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(); + this.config.onTransactionSent?.(hash); - if (steps.length > 0) { - throw new Error('Unexpected steps found'); - } + const receipt = await this.publicClient.waitForTransactionReceipt({ hash }); + debug('Transaction confirmed', receipt); - this.lastProps = props; - this.memoizedSteps = { - switchChain: { - isPending: !this.isOnCorrectChain(), - isExecuting: this.currentState === TransactionState.SWITCH_CHAIN, - execute: () => this.switchChain(), - }, - approval: { - isPending: Boolean(approvalStep), - isExecuting: this.currentState === TransactionState.TOKEN_APPROVAL, - execute: () => - approvalStep && this.executeStep({ step: approvalStep, props }), - }, + this.setTransactionState({ + ...this.transactionState!, transaction: { - isPending: Boolean(executionStep), - isExecuting: - this.currentState === TransactionState.EXECUTING_TRANSACTION, - execute: () => this.executeStep({ step: executionStep, props }), + ...this.transactionState!.transaction, + ready: false, + executing: false, + executed: true, }, - } as const; + }); + + this.config.onSuccess?.(hash); + } - debug('Generated new transaction steps', this.memoizedSteps); - return this.memoizedSteps; + kill() { + this.setTransactionState(null); + this.transactionState = null; } } diff --git a/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts b/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts index a2e4bd46..894cfdfe 100644 --- a/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts +++ b/packages/sdk/src/react/_internal/transaction-machine/useTransactionMachine.ts @@ -1,5 +1,5 @@ import { useSelectPaymentModal } from '@0xsequence/kit-checkout'; -import type { Hash } from 'viem'; +import type { Hash, Hex } from 'viem'; import { useAccount, useSwitchChain, useWalletClient } from 'wagmi'; import { getPublicRpcClient } from '../../../utils'; import { useConfig, useMarketplaceConfig } from '../../hooks'; @@ -8,28 +8,38 @@ import { WalletKind } from '../api'; import { type TransactionConfig, TransactionMachine, + TransactionState, } from './execute-transaction'; +import { useState } from 'react'; +import { useTransactionStatusModal } from '../../ui/modals/_internal/components/transactionStatusModal'; export type UseTransactionMachineConfig = Omit< TransactionConfig, 'sdkConfig' | 'marketplaceConfig' | 'walletKind' | 'chains' >; -export const useTransactionMachine = ( - config: UseTransactionMachineConfig, - onSuccess?: (hash: Hash) => void, - onError?: (error: Error) => void, - onTransactionSent?: (hash: Hash) => void, -) => { +export const useTransactionMachine = ({ + config, + closeActionModalCallback, + onSuccess, + onError, +}: { + config: UseTransactionMachineConfig; + closeActionModalCallback?: () => void; + onSuccess?: (hash: Hash) => void; + onError?: (error: Error) => void; +}) => { + const [transactionState, setTransactionState] = + useState(null); const { data: walletClient } = useWalletClient(); const { show: showSwitchChainModal } = useSwitchChainModal(); + const { show: showTransactionStatusModal } = useTransactionStatusModal(); const sdkConfig = useConfig(); const { data: marketplaceConfig, error: marketplaceError } = useMarketplaceConfig(); const { openSelectPaymentModal } = useSelectPaymentModal(); const { chains } = useSwitchChain(); - - const { connector } = useAccount(); + const { connector, chainId: accountChainId } = useAccount(); const walletKind = connector?.id === 'sequence' ? WalletKind.sequence : WalletKind.unknown; @@ -37,18 +47,18 @@ export const useTransactionMachine = ( throw marketplaceError; //TODO: Add error handling } - if (!walletClient || !marketplaceConfig) return null; + if (!walletClient || !marketplaceConfig || !accountChainId) return null; - return new TransactionMachine( + const transactionMachine = new TransactionMachine( { config: { sdkConfig, marketplaceConfig, walletKind, chains, ...config }, onSuccess, onError, - onTransactionSent, }, walletClient, getPublicRpcClient(config.chainId), openSelectPaymentModal, + accountChainId, async (chainId) => { return new Promise((resolve, reject) => { showSwitchChainModal({ @@ -59,8 +69,31 @@ export const useTransactionMachine = ( onError: (error) => { reject(error); }, + onClose: () => { + closeActionModalCallback?.(); + reject(new Error('User rejected chain switch')); + }, }); }); }, + transactionState, + setTransactionState, + closeActionModalCallback ?? (() => {}), + ({ hash, blocked }) => { + showTransactionStatusModal({ + chainId: String(accountChainId), + collectionAddress: config.collectionAddress as Hex, + collectibleId: config.collectibleId as string, + hash: hash, + type: config.transactionInput.type, + callbacks: { + onError: onError, + onSuccess: onSuccess, + }, + blocked, + }); + }, ); + + return transactionMachine; }; diff --git a/packages/sdk/src/react/hooks/index.ts b/packages/sdk/src/react/hooks/index.ts index 7fdfebb0..6701db8c 100644 --- a/packages/sdk/src/react/hooks/index.ts +++ b/packages/sdk/src/react/hooks/index.ts @@ -25,5 +25,9 @@ export * from './useTransferTokens'; export * from './useCheckoutOptions'; export * from './useListCollections'; export * from './useGenerateBuyTransaction'; -export * from './useCancelOrder'; -export * from './useBuyCollectable'; + +export * from './useMakeOffer'; +export * from './useCreateListing'; +export * from './useBuy'; +export * from './useSell'; +export * from './useCancel'; diff --git a/packages/sdk/src/react/hooks/useBuy.tsx b/packages/sdk/src/react/hooks/useBuy.tsx new file mode 100644 index 00000000..f4a214fc --- /dev/null +++ b/packages/sdk/src/react/hooks/useBuy.tsx @@ -0,0 +1,70 @@ +import { + useTransactionMachine, + UseTransactionMachineConfig, +} from '../_internal/transaction-machine/useTransactionMachine'; +import { + BuyInput, + TransactionType, +} from '../_internal/transaction-machine/execute-transaction'; +import { MarketplaceKind } from '../../types'; +import { ModalCallbacks } from '../ui/modals/_internal/types'; + +export default function useBuy({ + closeModalFn, + collectibleId, + collectionAddress, + chainId, + orderId, + collectableDecimals, + marketplace, + quantity, + callbacks, +}: { + closeModalFn: () => void; + collectibleId: string; + collectionAddress: string; + chainId: string; + orderId: string; + collectableDecimals: number; + marketplace: MarketplaceKind; + quantity: string; + callbacks: ModalCallbacks; +}) { + const buyProps = { + orderId, + collectableDecimals, + marketplace, + quantity, + } as BuyInput; + const machineConfig = { + transactionInput: { + type: TransactionType.BUY, + props: buyProps, + }, + collectionAddress, + chainId, + collectibleId, + type: TransactionType.BUY, + fetchStepsOnInitialize: true, + } as UseTransactionMachineConfig; + const machine = useTransactionMachine({ + config: machineConfig, + closeActionModalCallback: closeModalFn, + onSuccess: callbacks.onSuccess, + onError: callbacks.onError, + }); + + async function execute() { + if (!machine) return; + + await machine.execute({ + type: TransactionType.BUY, + props: buyProps, + }); + } + + return { + transactionState: machine?.transactionState, + execute, + }; +} 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/useCancel.tsx b/packages/sdk/src/react/hooks/useCancel.tsx new file mode 100644 index 00000000..38c92fef --- /dev/null +++ b/packages/sdk/src/react/hooks/useCancel.tsx @@ -0,0 +1,51 @@ +import { + useTransactionMachine, + UseTransactionMachineConfig, +} from '../_internal/transaction-machine/useTransactionMachine'; +import { + CancelInput, + TransactionType, +} from '../_internal/transaction-machine/execute-transaction'; +import { ModalCallbacks } from '../ui/modals/_internal/types'; + +export default function useCancel({ + collectionAddress, + chainId, + collectibleId, + onSuccess, + onError, +}: { + collectionAddress: string; + chainId: string; + collectibleId: string; + onSuccess?: ModalCallbacks['onSuccess']; + onError?: ModalCallbacks['onError']; +}) { + const machineConfig = { + chainId: chainId, + collectionAddress: collectionAddress, + collectibleId: collectibleId, + // no token approval (neither for nfts nor erc20) is needed for cancel transaction, hence executing is done without checking approval step + fetchStepsOnInitialize: false, + } as UseTransactionMachineConfig; + const machine = useTransactionMachine({ + config: machineConfig, + onSuccess, + onError, + }); + + async function execute(cancelProps: CancelInput) { + if (!machine) return; + + await machine.execute({ + type: TransactionType.CANCEL, + props: cancelProps, + }); + } + + return { + execute, + isLoading: machine?.transactionState?.transaction.executing, + executed: machine?.transactionState?.transaction.executed, + }; +} diff --git a/packages/sdk/src/react/hooks/useCancelOrder.tsx b/packages/sdk/src/react/hooks/useCancelOrder.tsx deleted file mode 100644 index 19112377..00000000 --- a/packages/sdk/src/react/hooks/useCancelOrder.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - type CancelInput, - TransactionType, -} from '../_internal/transaction-machine/execute-transaction'; -import { - useTransactionMachine, - type UseTransactionMachineConfig, -} from '../_internal/transaction-machine/useTransactionMachine'; - -interface UseCancelOrderArgs extends Omit { - onSuccess?: (hash: string) => void; - onError?: (error: Error) => void; - onTransactionSent?: (hash: string) => void; -} - -export const useCancelOrder = ({ - onSuccess, - onError, - onTransactionSent, - ...config -}: UseCancelOrderArgs) => { - const machine = useTransactionMachine( - { - ...config, - type: TransactionType.CANCEL, - }, - onSuccess, - onError, - onTransactionSent, - ); - - return { - cancel: (props: CancelInput) => machine?.start({ props }), - onError, - onSuccess, - onTransactionSent, - }; -}; diff --git a/packages/sdk/src/react/hooks/useCreateListing.tsx b/packages/sdk/src/react/hooks/useCreateListing.tsx index c2838357..3bdd300e 100644 --- a/packages/sdk/src/react/hooks/useCreateListing.tsx +++ b/packages/sdk/src/react/hooks/useCreateListing.tsx @@ -1,65 +1,98 @@ -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, + UseTransactionMachineConfig, } from '../_internal/transaction-machine/useTransactionMachine'; +import { + ListingInput, + TransactionType, +} from '../_internal/transaction-machine/execute-transaction'; +import { ContractType, Price, StepType } from '../../types'; +import { dateToUnixTime } from '../../utils/date'; +import { ModalCallbacks } from '../ui/modals/_internal/types'; -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, +export default function useCreateListing({ + closeModalFn, + collectionAddress, + chainId, + collectibleId, + collectionType, + expiry, + pricePerToken, + quantity, + callbacks, +}: { + closeModalFn: () => void; + collectionAddress: string; + chainId: string; + collectibleId: string; + collectionType: ContractType | undefined; + expiry: Date; + pricePerToken: Price; + quantity: string; + callbacks: ModalCallbacks; +}) { + const listingProps = { + tokenId: collectibleId, + expiry: dateToUnixTime(expiry), + currencyAddress: pricePerToken.currency.contractAddress, + quantity, + pricePerToken: pricePerToken.amountRaw, + } as ListingInput['listing']; + const machineConfig = { + transactionInput: { type: TransactionType.LISTING, + props: { + listing: listingProps, + contractType: collectionType as ContractType, + }, }, - onSuccess, - onError, - onTransactionSent, - ); + collectionAddress, + chainId, + collectibleId, + // no erc20 token approval needed, to move and transfer erc721s or erc1155s approval is needed + fetchStepsOnInitialize: true, + } as UseTransactionMachineConfig; - 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], - ); + const machine = useTransactionMachine({ + config: machineConfig, + closeActionModalCallback: closeModalFn, + onSuccess: callbacks.onSuccess, + onError: callbacks.onError, + }); + + async function approve() { + if (!machine?.transactionState) return; + + const steps = machine.transactionState.steps; + + if (!steps.steps) { + throw new Error('Steps is undefined, cannot find approval step'); + } + + const approvalStep = steps.steps.find( + (step) => step.id === StepType.tokenApproval, + ); + + await machine.approve({ + approvalStep: approvalStep!, + }); + } + + async function execute() { + if (!machine || !machine?.transactionState?.transaction.ready) return; + + await machine.execute({ + type: TransactionType.LISTING, + props: { + listing: listingProps, + contractType: collectionType as ContractType, + }, + }); + } return { - createListing: (props: ListingInput) => machine?.start({ props }), - getListingSteps: (props: ListingInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), - onError, - onSuccess, + transactionState: machine?.transactionState, + approve, + execute, }; -}; +} diff --git a/packages/sdk/src/react/hooks/useGenerateListingTransaction.tsx b/packages/sdk/src/react/hooks/useGenerateListingTransaction.tsx index 02ba8a1b..eb0c888a 100644 --- a/packages/sdk/src/react/hooks/useGenerateListingTransaction.tsx +++ b/packages/sdk/src/react/hooks/useGenerateListingTransaction.tsx @@ -14,6 +14,7 @@ import { type Step, getMarketplaceClient, } from '../_internal'; +import { dateToUnixTime } from '../../utils/date'; export type CreateReqWithDateExpiry = Omit & { expiry: Date; @@ -26,9 +27,6 @@ export type GenerateListingTransactionProps = Omit< listing: CreateReqWithDateExpiry; }; -const dateToUnixTime = (date: Date) => - Math.floor(date.getTime() / 1000).toString(); - export const generateListingTransaction = async ( params: GenerateListingTransactionProps, config: SdkConfig, diff --git a/packages/sdk/src/react/hooks/useGenerateOfferTransaction.tsx b/packages/sdk/src/react/hooks/useGenerateOfferTransaction.tsx index 7ea688c8..235b598a 100644 --- a/packages/sdk/src/react/hooks/useGenerateOfferTransaction.tsx +++ b/packages/sdk/src/react/hooks/useGenerateOfferTransaction.tsx @@ -8,6 +8,7 @@ import { getMarketplaceClient, } from '../_internal'; import { useConfig } from './useConfig'; +import { dateToUnixTime } from '../../utils/date'; export type UseGenerateOfferTransactionArgs = { chainId: ChainId; @@ -25,9 +26,6 @@ export type GenerateOfferTransactionProps = Omit< offer: CreateReqWithDateExpiry; }; -const dateToUnixTime = (date: Date) => - Math.floor(date.getTime() / 1000).toString(); - export const generateOfferTransaction = async ( params: GenerateOfferTransactionProps, config: SdkConfig, diff --git a/packages/sdk/src/react/hooks/useMakeOffer.tsx b/packages/sdk/src/react/hooks/useMakeOffer.tsx index c88f5152..348bc959 100644 --- a/packages/sdk/src/react/hooks/useMakeOffer.tsx +++ b/packages/sdk/src/react/hooks/useMakeOffer.tsx @@ -1,62 +1,97 @@ -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, + UseTransactionMachineConfig, } from '../_internal/transaction-machine/useTransactionMachine'; +import { + OfferInput, + TransactionType, +} from '../_internal/transaction-machine/execute-transaction'; +import { ContractType, Price, StepType } from '../../types'; +import { dateToUnixTime } from '../../utils/date'; +import { ModalCallbacks } from '../ui/modals/_internal/types'; -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, +export default function useMakeOffer({ + closeModal: closeModalFn, + collectionAddress, + chainId, + collectibleId, + collectionType, + offerPrice, + quantity, + expiry, + callbacks, +}: { + closeModal: () => void; + collectionAddress: string; + chainId: string; + collectibleId: string; + collectionType: ContractType | undefined; + offerPrice: Price; + quantity: string; + expiry: Date; + callbacks: ModalCallbacks; +}) { + const offerProps = { + tokenId: collectibleId, + quantity, + expiry: dateToUnixTime(expiry), + currencyAddress: offerPrice.currency.contractAddress, + pricePerToken: offerPrice.amountRaw, + } as OfferInput['offer']; + const machineConfig = { + transactionInput: { type: TransactionType.OFFER, + props: { + offer: offerProps, + contractType: collectionType as ContractType, + }, }, - 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], - ); + collectionAddress, + chainId, + collectibleId, + type: TransactionType.OFFER, + fetchStepsOnInitialize: false, + } as UseTransactionMachineConfig; + const machine = useTransactionMachine({ + config: machineConfig, + closeActionModalCallback: closeModalFn, + onSuccess: callbacks.onSuccess, + onError: callbacks.onError, + }); + + async function approve() { + if (!machine?.transactionState) return; + + const steps = machine.transactionState.steps; + + if (!steps.steps) { + throw new Error('Steps is undefined, cannot find approval step'); + } + + const approvalStep = steps.steps.find( + (step) => step.id === StepType.tokenApproval, + ); + + await machine.approve({ + approvalStep: approvalStep!, + }); + } + + async function execute() { + if (!machine) return; + + await machine.execute({ + type: TransactionType.OFFER, + props: { + offer: offerProps, + contractType: collectionType as ContractType, + }, + }); + } return { - makeOffer: (props: OfferInput) => machine?.start({ props }), - getMakeOfferSteps: (props: OfferInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), + transactionState: machine?.transactionState, + approve, + execute, }; -}; +} diff --git a/packages/sdk/src/react/hooks/useSell.tsx b/packages/sdk/src/react/hooks/useSell.tsx index 8f7672f4..873044c1 100644 --- a/packages/sdk/src/react/hooks/useSell.tsx +++ b/packages/sdk/src/react/hooks/useSell.tsx @@ -1,62 +1,85 @@ -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, + UseTransactionMachineConfig, } from '../_internal/transaction-machine/useTransactionMachine'; +import { + SellInput, + TransactionType, +} from '../_internal/transaction-machine/execute-transaction'; +import { MarketplaceKind, StepType } from '../../types'; +import { ModalCallbacks } from '../ui/modals/_internal/types'; -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, +export default function useSell({ + closeModalFn, + collectionAddress, + chainId, + collectibleId, + quantity, + orderId, + marketplace, + callbacks, +}: { + closeModalFn: () => void; + collectionAddress: string; + chainId: string; + collectibleId: string; + orderId?: string; + quantity?: string; + marketplace?: MarketplaceKind; + callbacks: ModalCallbacks; +}) { + const sellProps = { + orderId, + quantity, + marketplace, + } as SellInput; + const machineConfig = { + transactionInput: { type: TransactionType.SELL, + props: sellProps, }, - 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], - ); + collectionAddress, + chainId, + collectibleId, + fetchStepsOnInitialize: true, + } as UseTransactionMachineConfig; + const machine = useTransactionMachine({ + config: machineConfig, + closeActionModalCallback: closeModalFn, + onSuccess: callbacks.onSuccess, + onError: callbacks.onError, + }); + + async function approve() { + if (!machine?.transactionState) return; + + const steps = machine.transactionState.steps; + + if (!steps.steps) { + throw new Error('Steps is undefined, cannot find approval step'); + } + + const approvalStep = steps.steps.find( + (step) => step.id === StepType.tokenApproval, + ); + + await machine.approve({ + approvalStep: approvalStep!, + }); + } + + async function execute() { + if (!machine || !machine?.transactionState?.transaction.ready) return; + + await machine.execute({ + type: TransactionType.SELL, + props: sellProps, + }); + } return { - sell: (props: SellInput) => machine?.start({ props }), - getSellSteps: (props: SellInput) => ({ - isLoading, - steps, - refreshSteps: () => loadSteps(props), - }), + transactionState: machine?.transactionState, + approve, + execute, }; -}; +} 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..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 @@ -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/_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 6657d85c..7563bfbc 100644 --- a/packages/sdk/src/react/ui/modals/BuyModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/BuyModal/index.tsx @@ -1,19 +1,21 @@ import type { Hex } from 'viem'; import { buyModal$ } from './_store'; import { ContractType, MarketplaceKind, type Order } from '../../../_internal'; -import { observer, Show, useSelector } from '@legendapp/state/react'; +import { observer, Show } from '@legendapp/state/react'; import { useCollectible, useCollection } from '../../../hooks'; import { ActionModal } from '../_internal/components/actionModal'; import { useEffect } from 'react'; import QuantityInput from '..//_internal/components/quantityInput'; -import { useBuyCollectable } from '../../../hooks/useBuyCollectable'; import type { ModalCallbacks } from '../_internal/types'; import { TokenMetadata } from '@0xsequence/indexer'; +import useBuy from '../../../hooks/useBuy'; +import { LoadingModal } from '../_internal/components/actionModal/LoadingModal'; +import { ErrorModal } from '../_internal/components/actionModal/ErrorModal'; export type ShowBuyModalArgs = { chainId: string; collectionAddress: Hex; - tokenId: string; + collectibleId: string; order: Order; }; @@ -32,38 +34,75 @@ 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 { order, modalId } = buyModal$.get().state; + const callbacks = buyModal$.get().callbacks; + const chainId = String(order.chainId); + const collectionAddress = order.collectionContractAddress as Hex; + const collectibleId = order.tokenId; - const { data: collection } = useCollection({ - chainId, - collectionAddress, - }); - const { buy } = useBuyCollectable({ + const { + data: collection, + isLoading: collectionLoading, + isError: collectionError, + } = useCollection({ chainId, collectionAddress, }); - const { data: collectable } = useCollectible({ + const { + data: collectable, + isLoading: collectibleLoading, + isError: collectibleError, + } = useCollectible({ chainId, collectionAddress, collectibleId, }); + const { execute } = useBuy({ + closeModalFn: buyModal$.close, + collectibleId, + collectionAddress, + chainId, + orderId: order.orderId, + collectableDecimals: collectable?.decimals || 0, + marketplace: order.marketplace, + quantity: '1', + callbacks: callbacks || {}, + }); - if (modalId == 0 || !collection || !collectable || !buy) return null; + if (collectionLoading || collectibleLoading) { + return ( + + ); + } + + if (collectionError || collectibleError) { + return ( + + ); + } + + //TODO: Handle this better + if (modalId == 0 || !collection || !collectable) return null; return collection.type === ContractType.ERC721 ? ( ) : ( void; + }) => Promise; collectable: TokenMetadata; order: Order; } function CheckoutModal({ buy, collectable, order }: CheckoutModalProps) { useEffect(() => { - const executeBuy = () => { - console.log('executeBuy'); + const executeBuy = async() => { if (!collectable) return; - buy({ + + await buy({ orderId: order.orderId, collectableDecimals: collectable.decimals || 0, quantity: '1', marketplace: order.marketplace, }); + buyModal$.close(); }; @@ -110,38 +150,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/_utils/getCreateListingTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts deleted file mode 100644 index 76824ab1..00000000 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/_utils/getCreateListingTransactionTitleMessage.ts +++ /dev/null @@ -1,30 +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'; - } - - 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.`; - } - - return `You just listed ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx index c088844b..c5077d8e 100644 --- a/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/CreateListingModal/index.tsx @@ -1,14 +1,8 @@ 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 { useCreateListing } from '../../../hooks/useCreateListing'; +import { type ContractType } from '../../../_internal'; +import { useCollection } from '../../../hooks'; import { ActionModal, type ActionModalProps, @@ -21,13 +15,9 @@ 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'; +import useCreateListing from '../../../hooks/useCreateListing'; export type ShowCreateListingModalArgs = { collectionAddress: Hex; @@ -46,186 +36,139 @@ 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']; - }) => { - 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, - isError: collectionIsError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { getListingSteps } = useCreateListing({ - chainId, - 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) => { - 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); - } - }; - - if (collectableIsLoading || collectionIsLoading) { - return ( - - ); - } - - if (collectableIsError || collectionIsError) { - return ( - - ); - } - - 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']; +export const Modal = observer(() => { + const state = createListingModal$.get(); + const { + collectionAddress, + chainId, + listingPrice, + collectibleId, + expiry, + callbacks, + } = state; + const { + data: collection, + isLoading: collectionIsLoading, + isError: collectionIsError, + } = useCollection({ + chainId, + collectionAddress, + }); + const { transactionState, approve, execute } = useCreateListing({ + closeModalFn: createListingModal$.close, + collectionAddress, + chainId, + collectibleId, + collectionType: collection?.type as ContractType, + expiry: expiry, + pricePerToken: listingPrice, + quantity: state.quantity, + callbacks: callbacks || {}, + }); + + if (collectionIsLoading) { + return ( + + ); + } + if (collectionIsError) { return ( - createListingModal$.close()} + onClose={createListingModal$.close} title="List item for sale" - ctas={ctas} - > - + ); + } + + const checkingSteps = transactionState?.steps.checking; + + const ctas = [ + { + label: 'Approve TOKEN', + onClick: approve, + hidden: !transactionState?.approval.needed || checkingSteps, + pending: checkingSteps || transactionState?.approval.processing, + variant: 'glass' as const, + disabled: transactionState?.approval.processing, + }, + { + label: 'List item for sale', + onClick: execute, + pending: + !transactionState || + transactionState.steps.checking || + transactionState.transaction.executing, + disabled: + listingPrice.amountRaw === '0' || + !transactionState || + transactionState.steps.checking || + transactionState.approval.needed || + !transactionState.transaction.ready || + transactionState.transaction.executing, + }, + ] 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/_utils/getMakeOfferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts deleted file mode 100644 index 4af83a08..00000000 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/_utils/getMakeOfferTransactionTitleMessage.ts +++ /dev/null @@ -1,28 +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'; - } - - 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.`; - } - - return `You just made offer for ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx index a932bcd2..27e22b75 100644 --- a/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/MakeOfferModal/index.tsx @@ -1,25 +1,19 @@ import { Show, observer } from '@legendapp/state/react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import type { Hex } from 'viem'; -import { collectableKeys, ContractType, StepType } from '../../../_internal'; -import { useCollectible, useCollection, useCurrencies } from '../../../hooks'; -import { useMakeOffer } from '../../../hooks/useMakeOffer'; +import { ContractType } from '../../../_internal'; +import { useCollection, useCurrencies } from '../../../hooks'; import { ActionModal } from '../_internal/components/actionModal/ActionModal'; import ExpirationDateSelect from '../_internal/components/expirationDateSelect'; import FloorPriceText from '../_internal/components/floorPriceText'; import PriceInput from '../_internal/components/priceInput'; import QuantityInput from '../_internal/components/quantityInput'; import TokenPreview from '../_internal/components/tokenPreview'; -import { useTransactionStatusModal } from '../_internal/components/transactionStatusModal'; import { makeOfferModal$ } from './_store'; -import { - getMakeOfferTransactionMessage, - getMakeOfferTransactionTitle, -} from './_utils/getMakeOfferTransactionTitleMessage'; import { LoadingModal } from '../_internal/components/actionModal/LoadingModal'; import { ErrorModal } from '../_internal/components/actionModal/ErrorModal'; import type { ModalCallbacks } from '../_internal/types'; -import type { QueryKey } from '@tanstack/react-query'; +import useMakeOffer from '../../../hooks/useMakeOffer'; export type ShowMakeOfferModalArgs = { collectionAddress: Hex; @@ -34,202 +28,140 @@ export const useMakeOfferModal = (defaultCallbacks?: ModalCallbacks) => ({ }); 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, - isError: collectionIsError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { isLoading: currenciesIsLoading } = useCurrencies({ - chainId, - collectionAddress, - }); - - const { getMakeOfferSteps } = useMakeOffer({ - chainId, - 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) => { - 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 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 (collectableIsLoading || collectionIsLoading || currenciesIsLoading) { - return ( - - ); - } - - if (collectableIsError || collectionIsError) { - return ( - - ); - } - - 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, - }, - ]; +const ModalContent = observer(() => { + const state = makeOfferModal$.get(); + const { + collectionAddress, + chainId, + offerPrice, + collectibleId, + quantity, + expiry, + callbacks, + } = state; + const [insufficientBalance, setInsufficientBalance] = useState(false); + const { isLoading: currenciesIsLoading } = useCurrencies({ + chainId, + collectionAddress, + }); + const { + data: collection, + isLoading: collectionIsLoading, + isError: collectionIsError, + } = useCollection({ + chainId, + collectionAddress, + }); + const { transactionState, approve, execute } = useMakeOffer({ + closeModal: makeOfferModal$.close, + collectionAddress, + chainId, + collectibleId, + collectionType: collection?.type as ContractType, + offerPrice, + quantity, + expiry, + callbacks: callbacks || {}, + }); + + if (collectionIsLoading || currenciesIsLoading) { + return ( + + ); + } + if (collectionIsError) { return ( - makeOfferModal$.close()} + onClose={makeOfferModal$.close} title="Make an offer" - ctas={ctas} - > - + ); + } + + const checkingSteps = transactionState?.steps.checking; + + const ctas = [ + { + label: 'Approve TOKEN', + onClick: approve, + hidden: + !transactionState?.approval.needed || + transactionState?.approval.processed, + pending: checkingSteps || transactionState?.approval.processing, + variant: 'glass' as const, + disabled: checkingSteps || transactionState?.approval.processing, + }, + { + label: 'Make offer', + onClick: execute, + pending: + transactionState?.steps.checking || + transactionState?.transaction.executing, + disabled: + transactionState?.transaction.executing || + insufficientBalance || + offerPrice.amountRaw === '0' || + transactionState?.approval.processing || + transactionState?.approval.needed, + }, + ]; + + 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 deleted file mode 100644 index f61e31fe..00000000 --- a/packages/sdk/src/react/ui/modals/SellModal/_utils/getSellTransactionTitleMessage.ts +++ /dev/null @@ -1,28 +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'; - } - - 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.`; - } - - return `You just sold ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; diff --git a/packages/sdk/src/react/ui/modals/SellModal/index.tsx b/packages/sdk/src/react/ui/modals/SellModal/index.tsx index 8036068c..c566d56f 100644 --- a/packages/sdk/src/react/ui/modals/SellModal/index.tsx +++ b/packages/sdk/src/react/ui/modals/SellModal/index.tsx @@ -6,22 +6,11 @@ import TransactionDetails from '../_internal/components/transactionDetails'; import TransactionHeader from '../_internal/components/transactionHeader'; import { sellModal$ } from './_store'; import { useCollection, useCurrencies } from '../../../hooks'; -import { - balanceQueries, - collectableKeys, - StepType, - type Order, -} from '../../../_internal'; -import { useSell } from '../../../hooks/useSell'; +import { type Order } from '../../../_internal'; import { LoadingModal } from '../_internal/components/actionModal/LoadingModal'; import { ErrorModal } from '..//_internal/components/actionModal/ErrorModal'; import type { ModalCallbacks } from '..//_internal/types'; -import { - getSellTransactionMessage, - getSellTransactionTitle, -} from './_utils/getSellTransactionTitleMessage'; -import { useTransactionStatusModal } from '../_internal/components/transactionStatusModal'; -import type { QueryKey } from '@tanstack/react-query'; +import useSell from '../../../hooks/useSell'; export type ShowSellModalArgs = { chainId: string; @@ -37,154 +26,121 @@ export const useSellModal = (defaultCallbacks?: ModalCallbacks) => ({ }); 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) => { - 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); - } - } - }); - - const { - data: collection, - isLoading: collectionLoading, - isError: collectionError, - } = useCollection({ - chainId, - collectionAddress, - }); - - const { data: currencies, isLoading: currenciesLoading } = useCurrencies({ - chainId, - collectionAddress, - }); - - if (collectionLoading || currenciesLoading) { - return ( - - ); - } - - if (collectionError || order === undefined) { - return ( - - ); - } +const ModalContent = observer(() => { + const { tokenId, collectionAddress, chainId, order, callbacks } = + sellModal$.get(); + const { + data: collection, + isLoading: collectionLoading, + isError: collectionError, + } = useCollection({ + chainId, + collectionAddress, + }); + const { data: currencies, isLoading: currenciesLoading } = useCurrencies({ + chainId, + collectionAddress, + }); + const currency = currencies?.find( + (c) => c.contractAddress === order?.priceCurrencyAddress, + ); + const { transactionState, approve, execute } = useSell({ + closeModalFn: sellModal$.close, + collectionAddress, + chainId, + collectibleId: tokenId, + orderId: order!.orderId, + quantity: order!.quantityInitial, + marketplace: order!.marketplace, + callbacks: callbacks || {}, + }); - 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 checkingSteps = transactionState?.steps.checking; + + const ctas = [ + { + label: 'Approve TOKEN', + onClick: approve, + hidden: + !transactionState?.approval.needed || + transactionState?.approval.processed, + pending: checkingSteps || transactionState?.approval.processing, + variant: 'glass' as const, + disabled: checkingSteps || transactionState?.approval.processing, + }, + { + label: 'Accept', + onClick: execute, + pending: + transactionState?.steps.checking || + transactionState?.transaction.executing, + disabled: + !transactionState?.transaction.ready || + transactionState?.transaction.executing || + transactionState.approval.processing || + transactionState.approval.needed, + }, + ]; + + return ( + + + + - - ); - }, -); + : 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 1371e255..502e105e 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,18 @@ 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 +50,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/_utils/getTransferTransactionTitleMessage.ts b/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts deleted file mode 100644 index 9d9c080f..00000000 --- a/packages/sdk/src/react/ui/modals/TransferModal/_utils/getTransferTransactionTitleMessage.ts +++ /dev/null @@ -1,28 +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'; - } - - 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.`; - } - - return `You just transferred ${collectibleName}. It should be confirmed on the blockchain shortly.`; -}; 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..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 @@ -8,11 +8,19 @@ 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 } = - transferModal$.state.get(); + const { + collectionAddress, + collectibleId, + chainId, + collectionType, + callbacks, + } = transferModal$.state.get(); const $quantity = transferModal$.state.quantity; const isWalletAddressValid = isAddress( transferModal$.state.receiverAddress.get(), @@ -20,7 +28,7 @@ const EnterWalletAddressView = () => { const { data: tokenBalance } = useListBalances({ chainId, contractAddress: collectionAddress, - tokenId, + tokenId: collectibleId, accountAddress: address!, query: { enabled: !!address }, }); @@ -34,6 +42,7 @@ const EnterWalletAddressView = () => { collection?.type as CollectionType | undefined, ); const { transfer } = useHandleTransfer(); + const { show: showTransactionStatusModal } = useTransactionStatusModal(); function handleChangeWalletAddress( event: React.ChangeEvent, @@ -41,8 +50,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 +95,7 @@ const EnterWalletAddressView = () => { $quantity={$quantity} chainId={chainId} collectionAddress={collectionAddress} - collectibleId={tokenId} + collectibleId={collectibleId} /> { const { receiverAddress, collectionAddress, - tokenId, + collectibleId, quantity, chainId, collectionType, - successCallbacks, - errorCallbacks, + callbacks, } = transferModal$.state.get(); const { transferTokensAsync } = useTransferTokens(); - const { show: showTransactionStatusModal } = useTransactionStatusModal(); - const { data: collectible } = useCollectible({ - collectionAddress, - collectibleId: tokenId, - chainId, - }); async function transfer() { if ( @@ -42,33 +28,17 @@ const useHandleTransfer = () => { const hash = await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, - tokenId, + tokenId: collectibleId, chainId, contractType: ContractType.ERC721, }); 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[], - }); + return hash; } catch (error) { transferModal$.view.set('enterReceiverAddress'); - errorCallbacks?.onTransferError?.(error); + callbacks?.onError?.(error as Error); } } @@ -77,33 +47,18 @@ const useHandleTransfer = () => { const hash = await transferTokensAsync({ receiverAddress: receiverAddress as Hex, collectionAddress, - tokenId, + tokenId: collectibleId, chainId, contractType: ContractType.ERC1155, quantity: String(quantity), }); 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, - }, - }); + return hash; } catch (error) { transferModal$.view.set('enterReceiverAddress'); - errorCallbacks?.onTransferError?.(error); + callbacks?.onError?.(error as Error); } } } diff --git a/packages/sdk/src/react/ui/modals/TransferModal/index.tsx b/packages/sdk/src/react/ui/modals/TransferModal/index.tsx index a4a07ac5..6b30bcd2 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/actionModal/ActionModal.tsx b/packages/sdk/src/react/ui/modals/_internal/components/actionModal/ActionModal.tsx index 335b85a8..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 @@ -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'; @@ -29,7 +30,7 @@ export interface ActionModalProps { children: React.ReactNode; ctas: { label: string; - onClick: () => void; + onClick: (() => Promise) | (() => void); pending?: boolean; disabled?: boolean; hidden?: boolean; @@ -72,13 +73,14 @@ export const ActionModal = observer(