@0xsequence/marketplace-sdk 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/BellIcon.js +1 -1
  3. package/dist/Card.js +1 -1
  4. package/dist/ShopCard.d.ts +4 -4
  5. package/dist/builder-api.js +1 -1
  6. package/dist/collectible.js +1 -1
  7. package/dist/collection.js +1 -1
  8. package/dist/create-config.d.ts +597 -201
  9. package/dist/create-config.js +1 -1
  10. package/dist/currency.js +1 -1
  11. package/dist/dist.js +167 -148
  12. package/dist/dist.js.map +1 -1
  13. package/dist/expirationDateSelect.js +1 -1
  14. package/dist/filter-state.d.ts +1 -1
  15. package/dist/filters.d.ts +4 -4
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +2 -2
  18. package/dist/index10.d.ts +3 -3
  19. package/dist/index11.d.ts +17 -17
  20. package/dist/index12.d.ts +26 -26
  21. package/dist/index14.d.ts +3 -3
  22. package/dist/index15.d.ts +3 -3
  23. package/dist/index16.d.ts +2 -2
  24. package/dist/index17.d.ts +86 -86
  25. package/dist/index18.d.ts +40 -40
  26. package/dist/index19.d.ts +5 -5
  27. package/dist/index2.d.ts +4 -1
  28. package/dist/index21.d.ts +15 -15
  29. package/dist/index22.d.ts +8 -65
  30. package/dist/index23.d.ts +21 -13
  31. package/dist/index26.d.ts +4 -4
  32. package/dist/index27.d.ts +4 -4
  33. package/dist/index28.d.ts +10 -10
  34. package/dist/index3.d.ts +2 -2194
  35. package/dist/index31.d.ts +5 -5
  36. package/dist/index33.d.ts +3 -3
  37. package/dist/index34.d.ts +4 -4
  38. package/dist/index35.d.ts +1 -1
  39. package/dist/index36.d.ts +7 -7
  40. package/dist/index37.d.ts +6 -6
  41. package/dist/index38.d.ts +8 -8
  42. package/dist/index39.d.ts +1 -1
  43. package/dist/index4.d.ts +995 -995
  44. package/dist/index40.d.ts +2 -2
  45. package/dist/index8.d.ts +2 -2
  46. package/dist/index9.d.ts +2811 -3
  47. package/dist/inventory.d.ts +4 -4
  48. package/dist/inventory.js +2 -2
  49. package/dist/marketplace2.js +2 -2
  50. package/dist/metadata.d.ts +73 -73
  51. package/dist/primary-sale-checkout-options.d.ts +4 -4
  52. package/dist/quantityInput.js +1 -1
  53. package/dist/ranges.d.ts +17 -17
  54. package/dist/react/_internal/index.d.ts +1 -1
  55. package/dist/react/_internal/index.js +1 -1
  56. package/dist/react/index.d.ts +1 -1
  57. package/dist/react/queries/collectible/index.d.ts +1 -1
  58. package/dist/react/queries/index.d.ts +1 -1
  59. package/dist/react/ssr/index.d.ts +1 -1
  60. package/dist/react/ssr/index.js +2 -2
  61. package/dist/react/ui/components/marketplace-collectible-card/index.d.ts +1 -1
  62. package/dist/react/ui/components/marketplace-logos/index.d.ts +21 -21
  63. package/dist/react/ui/modals/CreateListingModal/internal/hooks/index.d.ts +1 -1
  64. package/dist/react/ui/modals/MakeOfferModal/internal/hooks/index.d.ts +1 -1
  65. package/dist/react/ui/modals/_internal/components/alertMessage/index.d.ts +2 -2
  66. package/dist/react/ui/modals/_internal/components/baseModal/index.d.ts +6 -6
  67. package/dist/react/ui/modals/_internal/components/calendar/index.d.ts +2 -2
  68. package/dist/react/ui/modals/_internal/components/calendarDropdown/index.d.ts +2 -2
  69. package/dist/react/ui/modals/_internal/components/currencyImage/index.d.ts +2 -2
  70. package/dist/react/ui/modals/_internal/components/currencyOptionsSelect/index.d.ts +3 -3
  71. package/dist/react/ui/modals/_internal/components/floorPriceText/index.d.ts +2 -2
  72. package/dist/react/ui/modals/_internal/components/priceInput/index.d.ts +3 -3
  73. package/dist/react/ui/modals/_internal/components/quantityInput/index.d.ts +2 -2
  74. package/dist/react/ui/modals/_internal/components/selectWaasFeeOptions/index.d.ts +2 -2
  75. package/dist/react/ui/modals/_internal/components/switchChainErrorModal/index.d.ts +2 -2
  76. package/dist/react/ui/modals/_internal/components/timeAgo/index.d.ts +2 -2
  77. package/dist/react/ui/modals/_internal/components/tokenPreview/index.d.ts +3 -3
  78. package/dist/react/ui/modals/_internal/components/transaction-footer/index.d.ts +3 -3
  79. package/dist/react/ui/modals/_internal/components/transactionDetails/index.d.ts +3 -3
  80. package/dist/react/ui/modals/_internal/components/transactionPreview/index.d.ts +3 -3
  81. package/dist/react/ui/modals/_internal/components/transactionStatusModal/index.d.ts +3 -3
  82. package/dist/react.js +2192 -1903
  83. package/dist/react.js.map +1 -1
  84. package/dist/token-balances.d.ts +28 -28
  85. package/dist/transaction-footer.js +1 -1
  86. package/dist/types/index.d.ts +1 -1
  87. package/dist/types/index.js +1 -1
  88. package/dist/types.d.ts +1 -1
  89. package/dist/url-state.js +1 -1
  90. package/dist/utils/index.js +1 -1
  91. package/dist/utils.js +20 -4
  92. package/dist/utils.js.map +1 -1
  93. package/package.json +7 -5
  94. package/src/react/hooks/config/useMarketplaceConfig.test.tsx +1 -0
  95. package/src/react/hooks/transactions/useCancelTransactionSteps.tsx +4 -1
  96. package/src/react/hooks/utils/useEnsureCorrectChain.ts +10 -5
  97. package/src/react/ui/modals/BuyModal/components/BuyModalContent.tsx +34 -31
  98. package/src/react/ui/modals/BuyModal/components/CryptoPaymentModal.tsx +74 -11
  99. package/src/react/ui/modals/BuyModal/components/Modal.tsx +62 -1
  100. package/src/react/ui/modals/BuyModal/hooks/useExecuteBundledTransactions.ts +13 -26
  101. package/src/react/ui/modals/BuyModal/internal/__tests__/buildTrailsMarketBuyActions.test.ts +213 -0
  102. package/src/react/ui/modals/BuyModal/internal/buildTrailsMarketBuyActions.ts +259 -0
  103. package/src/react/ui/modals/BuyModal/internal/buyModalContext.ts +79 -10
  104. package/src/react/ui/modals/BuyModal/internal/cryptoPaymentModalContext.tsx +44 -17
  105. package/src/react/ui/modals/MakeOfferModal/internal/context.ts +2 -1
  106. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/index.tsx +2 -1
  107. package/src/react/ui/modals/_internal/components/priceInput/index.tsx +12 -11
  108. package/src/react/ui/modals/_internal/helpers/currency.test.ts +27 -0
  109. package/src/react/ui/modals/_internal/helpers/currency.ts +4 -2
  110. package/src/utils/__tests__/getMarketplaceDetails.test.ts +10 -0
  111. package/src/utils/__tests__/getWebRPCErrorMessage.test.ts +28 -0
  112. package/src/utils/__tests__/marketplaceNormalization.test.ts +38 -0
  113. package/src/utils/getConduitAddressForOrderbook.ts +2 -10
  114. package/src/utils/getMarketplaceDetails.ts +11 -4
  115. package/src/utils/getWebRPCErrorMessage.ts +21 -0
  116. package/src/utils/normalizeMarketplace.ts +31 -0
@@ -0,0 +1,259 @@
1
+ import {
2
+ type Address,
3
+ type ApprovalStep,
4
+ type BuyStep,
5
+ ContractType,
6
+ type Hash,
7
+ MarketplaceKind,
8
+ type Order,
9
+ type OrderbookKind,
10
+ } from '@0xsequence/api-client';
11
+ import { buildErc20Approve, type Call, self } from '0xtrails';
12
+ import { encodeFunctionData, zeroAddress } from 'viem';
13
+ import { getConduitAddressForOrderbook } from '../../../../../utils/getConduitAddressForOrderbook';
14
+ import { normalizeMarketplaceKind } from '../../../../../utils/normalizeMarketplace';
15
+ import { OPENSEA_CHAIN_CURRENCIES } from '../../_internal/constants/opensea-currencies';
16
+
17
+ export type TrailsMarketBuyCall = Call;
18
+
19
+ export type TrailsMarketBuyActions = {
20
+ calls: TrailsMarketBuyCall[];
21
+ paymentTokenAddress: Address;
22
+ paymentAmount: bigint;
23
+ };
24
+
25
+ type BuildTrailsMarketBuyActionsParams = {
26
+ chainId: number;
27
+ buyStep: BuyStep;
28
+ marketOrder: Order;
29
+ contractType: ContractType.ERC721 | ContractType.ERC1155;
30
+ recipientAddress: Address;
31
+ approvalStep?: ApprovalStep;
32
+ quantity?: bigint;
33
+ };
34
+
35
+ const WETH_ABI = [
36
+ {
37
+ type: 'function',
38
+ name: 'withdraw',
39
+ stateMutability: 'nonpayable',
40
+ inputs: [{ name: 'wad', type: 'uint256' }],
41
+ outputs: [],
42
+ },
43
+ ] as const;
44
+
45
+ const ERC721_TRANSFER_FROM_ABI = [
46
+ {
47
+ type: 'function',
48
+ name: 'transferFrom',
49
+ stateMutability: 'nonpayable',
50
+ inputs: [
51
+ { name: 'from', type: 'address' },
52
+ { name: 'to', type: 'address' },
53
+ { name: 'tokenId', type: 'uint256' },
54
+ ],
55
+ outputs: [],
56
+ },
57
+ ] as const;
58
+
59
+ const ERC1155_SAFE_TRANSFER_FROM_ABI = [
60
+ {
61
+ type: 'function',
62
+ name: 'safeTransferFrom',
63
+ stateMutability: 'nonpayable',
64
+ inputs: [
65
+ { name: 'from', type: 'address' },
66
+ { name: 'to', type: 'address' },
67
+ { name: 'id', type: 'uint256' },
68
+ { name: 'amount', type: 'uint256' },
69
+ { name: 'data', type: 'bytes' },
70
+ ],
71
+ outputs: [],
72
+ },
73
+ ] as const;
74
+
75
+ const OPENSEA_FULFILL_WITHOUT_RECIPIENT_SELECTORS = new Set([
76
+ '0xfb0f3ee1', // fulfillBasicOrder(...)
77
+ '0xb1b747c3', // fulfillOrder(...)
78
+ ]);
79
+
80
+ const isNativeCurrency = (currencyAddress: Address) =>
81
+ currencyAddress.toLowerCase() === zeroAddress;
82
+
83
+ const getPaymentAmount = ({
84
+ buyStep,
85
+ marketOrder,
86
+ isErc20Payment,
87
+ }: {
88
+ buyStep: BuyStep;
89
+ marketOrder: Order;
90
+ isErc20Payment: boolean;
91
+ }) => {
92
+ if (isErc20Payment) {
93
+ return buyStep.price > 0n ? buyStep.price : marketOrder.priceAmount;
94
+ }
95
+
96
+ if (buyStep.value > 0n) {
97
+ return buyStep.value;
98
+ }
99
+
100
+ return buyStep.price > 0n ? buyStep.price : marketOrder.priceAmount;
101
+ };
102
+
103
+ const getWrappedNativeCurrencyAddress = (chainId: number) =>
104
+ OPENSEA_CHAIN_CURRENCIES[chainId.toString()]?.wrappedNativeCurrency.address as
105
+ | Address
106
+ | undefined;
107
+
108
+ const buildWrappedNativeWithdrawCall = ({
109
+ wrappedNativeAddress,
110
+ amount,
111
+ }: {
112
+ wrappedNativeAddress: Address;
113
+ amount: bigint;
114
+ }): TrailsMarketBuyCall => ({
115
+ to: wrappedNativeAddress,
116
+ data: encodeFunctionData({
117
+ abi: WETH_ABI,
118
+ functionName: 'withdraw',
119
+ args: [amount],
120
+ }),
121
+ });
122
+
123
+ const isOpenSeaFulfillWithoutRecipient = (calldata: Hash) =>
124
+ OPENSEA_FULFILL_WITHOUT_RECIPIENT_SELECTORS.has(
125
+ calldata.slice(0, 10).toLowerCase(),
126
+ );
127
+
128
+ const buildOpenSeaNftTransferCall = ({
129
+ marketOrder,
130
+ contractType,
131
+ recipientAddress,
132
+ quantity,
133
+ }: {
134
+ marketOrder: Order;
135
+ contractType: ContractType.ERC721 | ContractType.ERC1155;
136
+ recipientAddress: Address;
137
+ quantity: bigint;
138
+ }): TrailsMarketBuyCall | undefined => {
139
+ if (marketOrder.tokenId === undefined) {
140
+ return undefined;
141
+ }
142
+
143
+ const collectionAddress = marketOrder.collectionContractAddress as Address;
144
+ const executorAddress = self();
145
+
146
+ if (contractType === ContractType.ERC1155) {
147
+ return {
148
+ to: collectionAddress,
149
+ data: encodeFunctionData({
150
+ abi: ERC1155_SAFE_TRANSFER_FROM_ABI,
151
+ functionName: 'safeTransferFrom',
152
+ args: [
153
+ executorAddress,
154
+ recipientAddress,
155
+ marketOrder.tokenId,
156
+ quantity,
157
+ '0x',
158
+ ],
159
+ }),
160
+ };
161
+ }
162
+
163
+ return {
164
+ to: collectionAddress,
165
+ data: encodeFunctionData({
166
+ abi: ERC721_TRANSFER_FROM_ABI,
167
+ functionName: 'transferFrom',
168
+ args: [executorAddress, recipientAddress, marketOrder.tokenId],
169
+ }),
170
+ };
171
+ };
172
+
173
+ export function buildTrailsMarketBuyActions({
174
+ chainId,
175
+ buyStep,
176
+ marketOrder,
177
+ contractType,
178
+ recipientAddress,
179
+ approvalStep,
180
+ quantity = 1n,
181
+ }: BuildTrailsMarketBuyActionsParams): TrailsMarketBuyActions | undefined {
182
+ const calls: TrailsMarketBuyCall[] = [];
183
+ const currencyAddress = marketOrder.priceCurrencyAddress;
184
+ const isErc20Payment = !isNativeCurrency(currencyAddress);
185
+ const paymentAmount = getPaymentAmount({
186
+ buyStep,
187
+ marketOrder,
188
+ isErc20Payment,
189
+ });
190
+ let paymentTokenAddress = currencyAddress;
191
+
192
+ if (isErc20Payment && paymentAmount > 0n) {
193
+ if (approvalStep) {
194
+ calls.push({
195
+ to: approvalStep.to,
196
+ data: approvalStep.data,
197
+ });
198
+ } else {
199
+ const spenderAddress = getConduitAddressForOrderbook(
200
+ marketOrder.marketplace as unknown as OrderbookKind,
201
+ );
202
+
203
+ if (spenderAddress) {
204
+ calls.push(
205
+ buildErc20Approve({
206
+ tokenAddress: currencyAddress,
207
+ spender: spenderAddress,
208
+ amount: paymentAmount,
209
+ }),
210
+ );
211
+ }
212
+ }
213
+ } else if (!isErc20Payment) {
214
+ const wrappedNativeAddress = getWrappedNativeCurrencyAddress(chainId);
215
+ if (!wrappedNativeAddress) {
216
+ return undefined;
217
+ }
218
+
219
+ paymentTokenAddress = wrappedNativeAddress;
220
+ if (paymentAmount > 0n) {
221
+ calls.push(
222
+ buildWrappedNativeWithdrawCall({
223
+ wrappedNativeAddress,
224
+ amount: paymentAmount,
225
+ }),
226
+ );
227
+ }
228
+ }
229
+
230
+ calls.push({
231
+ to: buyStep.to,
232
+ data: buyStep.data,
233
+ ...(!isErc20Payment && paymentAmount > 0n
234
+ ? { value: paymentAmount, sweepTokens: [zeroAddress] }
235
+ : {}),
236
+ });
237
+
238
+ const isOpenSeaOrder =
239
+ normalizeMarketplaceKind(marketOrder.marketplace) ===
240
+ MarketplaceKind.opensea;
241
+ if (isOpenSeaOrder && isOpenSeaFulfillWithoutRecipient(buyStep.data)) {
242
+ const nftTransferCall = buildOpenSeaNftTransferCall({
243
+ marketOrder,
244
+ contractType,
245
+ recipientAddress,
246
+ quantity,
247
+ });
248
+
249
+ if (nftTransferCall) {
250
+ calls.push(nftTransferCall);
251
+ }
252
+ }
253
+
254
+ return {
255
+ calls,
256
+ paymentTokenAddress,
257
+ paymentAmount,
258
+ };
259
+ }
@@ -1,6 +1,14 @@
1
- import { ContractType, type Hash } from '@0xsequence/api-client';
2
- import { useSupportedChains } from '0xtrails';
1
+ import {
2
+ type Address,
3
+ ContractType,
4
+ findApprovalStep,
5
+ findBuyStep,
6
+ type Hash,
7
+ } from '@0xsequence/api-client';
8
+ import { encodeDestinationCalls, useSupportedChains } from '0xtrails';
9
+ import { useMemo } from 'react';
3
10
  import { type Chain, formatUnits } from 'viem';
11
+ import { useAccount } from 'wagmi';
4
12
  import { TransactionType } from '../../../../_internal';
5
13
  import { useConfig } from '../../../../hooks';
6
14
  import { useBuyTransaction } from '../../../../hooks/transactions/useBuyTransaction';
@@ -10,15 +18,25 @@ import { useTransactionStatusModal } from '../../_internal/components/transactio
10
18
  import { useBuyModal } from '..';
11
19
  import { useBuyModalData } from '../hooks/useBuyModalData';
12
20
  import { useBuyModalProps } from '../store';
21
+ import { buildTrailsMarketBuyActions } from './buildTrailsMarketBuyActions';
13
22
  import { determineCheckoutMode } from './determineCheckoutMode';
14
23
 
24
+ type TrailsDestination = {
25
+ recipient: Address;
26
+ destinationCalldata: Hash;
27
+ paymentTokenAddress: Address;
28
+ paymentAmount: bigint;
29
+ };
30
+
15
31
  export function useBuyModalContext() {
16
32
  const config = useConfig();
17
33
  const modalProps = useBuyModalProps();
18
34
  const checkoutModeConfig: CheckoutMode = config.checkoutMode ?? 'trails';
19
35
  const { close } = useBuyModal();
20
36
  const transactionStatusModal = useTransactionStatusModal();
21
- const { supportedChains, isLoadingChains } = useSupportedChains();
37
+ const { address: userWalletAddress } = useAccount();
38
+ const { data: supportedChains = [], isLoading: isLoadingChains } =
39
+ useSupportedChains();
22
40
 
23
41
  const {
24
42
  collectible,
@@ -34,16 +52,18 @@ export function useBuyModalContext() {
34
52
  refetchQueries,
35
53
  } = useBuyModalData();
36
54
 
55
+ const contractType =
56
+ collection?.type === ContractType.ERC1155
57
+ ? ContractType.ERC1155
58
+ : ContractType.ERC721;
59
+
37
60
  const transactionData = useBuyTransaction({
38
61
  modalProps,
39
62
  primarySalePrice: {
40
63
  amount: primarySaleItem?.priceAmount,
41
64
  currencyAddress: primarySaleItem?.currencyAddress,
42
65
  },
43
- contractType:
44
- collection?.type === ContractType.ERC1155
45
- ? ContractType.ERC1155
46
- : ContractType.ERC721,
66
+ contractType,
47
67
  });
48
68
  const steps = transactionData.data?.steps;
49
69
  const canBeUsedWithTrails =
@@ -58,7 +78,47 @@ export function useBuyModalContext() {
58
78
 
59
79
  const isLoading = isLoadingSteps || isLoadingChains || isBuyModalDataLoading;
60
80
 
61
- const buyStep = steps?.find((step) => step.id === 'buy');
81
+ const buyStep = steps ? findBuyStep(steps) : undefined;
82
+ const approvalStep = steps ? findApprovalStep(steps) : undefined;
83
+
84
+ const trailsDestination = useMemo<TrailsDestination | undefined>(() => {
85
+ if (!isMarket || !marketOrder || !buyStep || !userWalletAddress) {
86
+ return undefined;
87
+ }
88
+
89
+ const trailsMarketBuyActions = buildTrailsMarketBuyActions({
90
+ chainId: modalProps.chainId,
91
+ buyStep,
92
+ marketOrder,
93
+ contractType,
94
+ recipientAddress: userWalletAddress,
95
+ approvalStep,
96
+ });
97
+ if (!trailsMarketBuyActions) {
98
+ return undefined;
99
+ }
100
+
101
+ const destination = encodeDestinationCalls({
102
+ calls: trailsMarketBuyActions.calls,
103
+ tokenAddress: trailsMarketBuyActions.paymentTokenAddress,
104
+ sweepTarget: userWalletAddress,
105
+ });
106
+
107
+ return {
108
+ recipient: destination.recipient,
109
+ destinationCalldata: destination.destinationCalldata,
110
+ paymentTokenAddress: trailsMarketBuyActions.paymentTokenAddress,
111
+ paymentAmount: trailsMarketBuyActions.paymentAmount,
112
+ };
113
+ }, [
114
+ approvalStep,
115
+ buyStep,
116
+ contractType,
117
+ isMarket,
118
+ marketOrder,
119
+ modalProps.chainId,
120
+ userWalletAddress,
121
+ ]);
62
122
 
63
123
  const checkoutMode = determineCheckoutMode({
64
124
  checkoutModeConfig,
@@ -66,9 +126,16 @@ export function useBuyModalContext() {
66
126
  canBeUsedWithTrails,
67
127
  });
68
128
 
129
+ const paymentAmount =
130
+ trailsDestination?.paymentAmount ??
131
+ (buyStep
132
+ ? buyStep.price > 0n
133
+ ? buyStep.price
134
+ : buyStep.value
135
+ : undefined);
69
136
  const formattedAmount =
70
- currency?.decimals && buyStep?.price
71
- ? formatUnits(BigInt(buyStep.price), currency.decimals)
137
+ currency?.decimals !== undefined && paymentAmount !== undefined
138
+ ? formatUnits(paymentAmount, currency.decimals)
72
139
  : undefined;
73
140
 
74
141
  const handleTransactionSuccess = (hash: Hash) => {
@@ -131,8 +198,10 @@ export function useBuyModalContext() {
131
198
  collection,
132
199
  primarySaleItem,
133
200
  marketOrder,
201
+ isMarket,
134
202
  isShop,
135
203
  buyStep,
204
+ trailsDestination,
136
205
  isLoading,
137
206
  checkoutMode,
138
207
  formattedAmount,
@@ -2,8 +2,9 @@ import type { Address, Hash } from '@0xsequence/api-client';
2
2
  import { ChevronLeftIcon, Text } from '@0xsequence/design-system';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useState } from 'react';
5
- import type { Hex } from 'viem';
5
+ import { zeroAddress, type Hex } from 'viem';
6
6
  import { useSendTransaction } from 'wagmi';
7
+ import { getErrorMessage } from '../../../../../utils/getErrorMessage';
7
8
  import { formatPrice } from '../../../../../utils/price';
8
9
  import { type Step, StepType } from '../../../../_internal';
9
10
  import { useConnectorMetadata } from '../../../../hooks';
@@ -74,6 +75,31 @@ type CryptoPaymentModalReturn = {
74
75
  };
75
76
  };
76
77
 
78
+ const toErrorWithDetails = (error: unknown): Error => {
79
+ if (error instanceof Error) {
80
+ return error;
81
+ }
82
+
83
+ if (typeof error === 'object' && error !== null) {
84
+ const errorLike = error as Record<string, unknown>;
85
+ let message = 'An unexpected error occurred.';
86
+
87
+ if (typeof errorLike.message === 'string') {
88
+ message = errorLike.message;
89
+ } else {
90
+ try {
91
+ message = JSON.stringify(errorLike);
92
+ } catch {
93
+ message = 'An unexpected error occurred.';
94
+ }
95
+ }
96
+
97
+ return Object.assign(new Error(message), errorLike);
98
+ }
99
+
100
+ return new Error(String(error));
101
+ };
102
+
77
103
  export function useCryptoPaymentModalContext({
78
104
  chainId,
79
105
  steps,
@@ -101,7 +127,6 @@ export function useCryptoPaymentModalContext({
101
127
  marketOrder,
102
128
  primarySaleItem,
103
129
  isMarket,
104
- isShop,
105
130
  collection,
106
131
  isLoading: isLoadingBuyModalData,
107
132
  } = useBuyModalData();
@@ -115,11 +140,16 @@ export function useCryptoPaymentModalContext({
115
140
  const priceCurrencyAddress = isMarket
116
141
  ? marketOrder?.priceCurrencyAddress
117
142
  : (primarySaleItem?.currencyAddress as Address);
143
+ const listedPriceAmount = BigInt(priceAmount || 0);
144
+ const buyStepPriceAmount = buyStep.price ? BigInt(buyStep.price) : listedPriceAmount;
145
+ const buyStepValue = buyStep.value ? BigInt(buyStep.value) : buyStepPriceAmount;
146
+ const requiredBalance =
147
+ priceCurrencyAddress === zeroAddress ? buyStepValue : buyStepPriceAmount;
118
148
  const isAnyTransactionPending = isApproving || isExecuting;
119
149
 
120
150
  const { data, isLoading: isLoadingBalance } = useHasSufficientBalance({
121
151
  chainId,
122
- value: BigInt(priceAmount || 0),
152
+ value: requiredBalance,
123
153
  tokenAddress: priceCurrencyAddress as Address,
124
154
  });
125
155
 
@@ -137,7 +167,6 @@ export function useCryptoPaymentModalContext({
137
167
  } = useExecuteBundledTransactions({
138
168
  chainId,
139
169
  approvalStep,
140
- priceAmount: BigInt(priceAmount || 0),
141
170
  });
142
171
 
143
172
  const waas = useSelectWaasFeeOptionsStore();
@@ -169,7 +198,10 @@ export function useCryptoPaymentModalContext({
169
198
  throw errorDetails;
170
199
  }
171
200
 
172
- await ensureCorrectChainAsync(chainId);
201
+ const isOnCorrectChain = await ensureCorrectChainAsync(chainId);
202
+ if (!isOnCorrectChain) {
203
+ throw new Error('Failed to switch to the required network.');
204
+ }
173
205
 
174
206
  const hash = await sendTransactionAsync({
175
207
  to,
@@ -195,12 +227,10 @@ export function useCryptoPaymentModalContext({
195
227
  await executeTransaction(approvalStep);
196
228
  setApprovalStep(undefined);
197
229
  } catch (error) {
198
- const errorObj =
199
- error instanceof Error ? error : new Error(String(error));
230
+ const errorObj = toErrorWithDetails(error);
200
231
  setError({
201
232
  title: 'Approval failed',
202
- message:
203
- errorObj.message || 'Failed to approve token. Please try again.',
233
+ message: getErrorMessage(errorObj),
204
234
  details: errorObj,
205
235
  });
206
236
  console.error('Approval transaction failed:', error);
@@ -222,7 +252,7 @@ export function useCryptoPaymentModalContext({
222
252
  const handleTransactionFailed = (error: Error) => {
223
253
  setError({
224
254
  title: 'Transaction failed',
225
- message: error.message,
255
+ message: getErrorMessage(error),
226
256
  details: error,
227
257
  });
228
258
 
@@ -242,12 +272,10 @@ export function useCryptoPaymentModalContext({
242
272
 
243
273
  onSuccess(hash as Hash);
244
274
  } catch (error) {
245
- const errorObj =
246
- error instanceof Error ? error : new Error(String(error));
275
+ const errorObj = toErrorWithDetails(error);
247
276
  setError({
248
277
  title: 'Purchase failed',
249
- message:
250
- errorObj.message || 'Failed to complete purchase. Please try again.',
278
+ message: getErrorMessage(errorObj),
251
279
  details: errorObj,
252
280
  });
253
281
  console.error('Buy transaction failed:', error);
@@ -261,7 +289,7 @@ export function useCryptoPaymentModalContext({
261
289
  };
262
290
 
263
291
  const formattedPrice = formatPrice(
264
- BigInt(priceAmount || 0),
292
+ buyStepPriceAmount,
265
293
  currency?.decimals || 0,
266
294
  );
267
295
  const isFree = formattedPrice === '0';
@@ -330,7 +358,6 @@ export function useCryptoPaymentModalContext({
330
358
  !isAnyTransactionPending &&
331
359
  !isFeeSelectionVisible;
332
360
 
333
- const needsBundledTransactions = isShop && !!approvalStep;
334
361
  const canBuy =
335
362
  hasSufficientBalance &&
336
363
  !isLoadingBalance &&
@@ -338,7 +365,7 @@ export function useCryptoPaymentModalContext({
338
365
  (isSequenceConnector ? true : !approvalStep) &&
339
366
  !isAnyTransactionPending &&
340
367
  !isFeeSelectionVisible &&
341
- (needsBundledTransactions ? isBundledTransactionsReady : true);
368
+ isBundledTransactionsReady;
342
369
 
343
370
  const result: CryptoPaymentModalReturn = {
344
371
  data: {
@@ -5,6 +5,7 @@ import { zeroAddress } from 'viem';
5
5
  import { useAccount } from 'wagmi';
6
6
  import { dateToUnixTime } from '../../../../../utils/date';
7
7
  import { getConduitAddressForOrderbook } from '../../../../../utils/getConduitAddressForOrderbook';
8
+ import { isOpenSeaOrderbook } from '../../../../../utils/normalizeMarketplace';
8
9
  import {
9
10
  useCollectibleMarketLowestListing,
10
11
  useCollectibleMetadata,
@@ -113,7 +114,7 @@ export function useMakeOfferModalContext() {
113
114
  amountRaw: state.priceInput?.toString(),
114
115
  query: {
115
116
  enabled:
116
- state.orderbookKind === OrderbookKind.opensea &&
117
+ isOpenSeaOrderbook(state.orderbookKind) &&
117
118
  !!selectedCurrency?.contractAddress &&
118
119
  !!state.priceInput,
119
120
  },
@@ -4,6 +4,7 @@ import type { Address } from '@0xsequence/api-client';
4
4
  import { Skeleton } from '@0xsequence/design-system';
5
5
  import { useEffect } from 'react';
6
6
  import { compareAddress } from '../../../../../../utils';
7
+ import { isOpenSeaOrderbook } from '../../../../../../utils/normalizeMarketplace';
7
8
  import { type Currency, OrderbookKind } from '../../../../../_internal';
8
9
  import { useCurrencyList } from '../../../../../hooks';
9
10
  import {
@@ -41,7 +42,7 @@ function CurrencyOptionsSelect({
41
42
 
42
43
  // Filter currencies for OpenSea
43
44
  let filteredCurrencies = currencies;
44
- if (currencies && orderbookKind === OrderbookKind.opensea && modalType) {
45
+ if (currencies && isOpenSeaOrderbook(orderbookKind) && modalType) {
45
46
  const openseaCurrency = getOpenseaCurrencyForChain(chainId, modalType);
46
47
  if (openseaCurrency) {
47
48
  // Filter to only show the OpenSea-supported currency
@@ -15,6 +15,7 @@ import { useAccount } from 'wagmi';
15
15
  import type { Currency, Price } from '../../../../../../types';
16
16
  import { calculateTotalOfferCost, cn } from '../../../../../../utils';
17
17
  import { validateOpenseaOfferDecimals } from '../../../../../../utils/price';
18
+ import { isOpenSeaOrderbook } from '../../../../../../utils/normalizeMarketplace';
18
19
  import {
19
20
  useConvertPriceToUSD,
20
21
  useTokenCurrencyBalance,
@@ -74,13 +75,13 @@ export default function PriceInput({
74
75
  useConvertPriceToUSD({
75
76
  chainId,
76
77
  currencyAddress: currencyAddress ?? zeroAddress,
77
- amountRaw: priceAmountRaw?.toString(),
78
- query: {
79
- enabled:
80
- orderbookKind === OrderbookKind.opensea &&
81
- !!currencyAddress &&
82
- !!priceAmountRaw,
83
- },
78
+ amountRaw: priceAmountRaw?.toString(),
79
+ query: {
80
+ enabled:
81
+ isOpenSeaOrderbook(orderbookKind) &&
82
+ !!currencyAddress &&
83
+ !!priceAmountRaw,
84
+ },
84
85
  });
85
86
 
86
87
  useEffect(() => {
@@ -160,7 +161,7 @@ export default function PriceInput({
160
161
  );
161
162
 
162
163
  const openseaLowestPriceCriteriaMet =
163
- orderbookKind === OrderbookKind.opensea &&
164
+ isOpenSeaOrderbook(orderbookKind) &&
164
165
  conversion?.usdAmount !== undefined &&
165
166
  conversion.usdAmount >= 0.01;
166
167
 
@@ -205,7 +206,7 @@ export default function PriceInput({
205
206
  if (!price || !onPriceChange) return;
206
207
 
207
208
  // Validate OpenSea decimal constraints for offers
208
- if (orderbookKind === OrderbookKind.opensea && modalType === 'offer') {
209
+ if (isOpenSeaOrderbook(orderbookKind) && modalType === 'offer') {
209
210
  const validation = validateOpenseaOfferDecimals(newValue);
210
211
  if (!validation.isValid) {
211
212
  setOpenseaDecimalError(validation.errorMessage || null);
@@ -318,7 +319,7 @@ export default function PriceInput({
318
319
  {!balanceError &&
319
320
  priceAmountRaw !== 0n &&
320
321
  !openseaLowestPriceCriteriaMet &&
321
- orderbookKind === OrderbookKind.opensea &&
322
+ isOpenSeaOrderbook(orderbookKind) &&
322
323
  !isConversionLoading &&
323
324
  modalType === 'offer' &&
324
325
  !openseaDecimalError && (
@@ -332,7 +333,7 @@ export default function PriceInput({
332
333
 
333
334
  {!balanceError &&
334
335
  openseaDecimalError &&
335
- orderbookKind === OrderbookKind.opensea &&
336
+ isOpenSeaOrderbook(orderbookKind) &&
336
337
  modalType === 'offer' && (
337
338
  <Text className="font-body font-medium text-xs" color="negative">
338
339
  {openseaDecimalError}
@@ -0,0 +1,27 @@
1
+ import { OrderbookKind } from '@0xsequence/api-client';
2
+ import * as MarketplaceMocks from '@0xsequence/api-client/mocks/marketplace';
3
+ import { describe, expect, it } from 'vitest';
4
+ import type { Currency } from '../../../../_internal';
5
+ import { filterCurrenciesForOrderbook } from './currency';
6
+
7
+ const { mockCurrencies } = MarketplaceMocks;
8
+ const typedMockCurrencies: Currency[] = mockCurrencies.map((currency) => ({
9
+ ...currency,
10
+ contractAddress: currency.contractAddress as `0x${string}`,
11
+ }));
12
+
13
+ describe('filterCurrenciesForOrderbook', () => {
14
+ it('should apply OpenSea listing currency limits to Magic Eden orderbooks', () => {
15
+ const filteredCurrencies = filterCurrenciesForOrderbook(
16
+ typedMockCurrencies,
17
+ OrderbookKind.magic_eden,
18
+ 1,
19
+ 'listing',
20
+ );
21
+
22
+ expect(filteredCurrencies).toHaveLength(1);
23
+ expect(filteredCurrencies[0]?.contractAddress).toBe(
24
+ typedMockCurrencies[0]?.contractAddress,
25
+ );
26
+ });
27
+ });