@0xsequence/marketplace-sdk 2.0.0 → 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 (136) hide show
  1. package/CHANGELOG.md +18 -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 +2 -2
  7. package/dist/collectible.js.map +1 -1
  8. package/dist/collection.js +1 -1
  9. package/dist/create-config.d.ts +589 -193
  10. package/dist/create-config.js +1 -1
  11. package/dist/currency.js +3 -3
  12. package/dist/currency.js.map +1 -1
  13. package/dist/dist.js +167 -148
  14. package/dist/dist.js.map +1 -1
  15. package/dist/expirationDateSelect.js +1 -1
  16. package/dist/filter-state.d.ts +1 -1
  17. package/dist/filters.d.ts +1 -1
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +3 -3
  20. package/dist/index10.d.ts +1 -1
  21. package/dist/index11.d.ts +17 -17
  22. package/dist/index12.d.ts +21 -21
  23. package/dist/index14.d.ts +3 -3
  24. package/dist/index15.d.ts +3 -3
  25. package/dist/index16.d.ts +2 -2
  26. package/dist/index17.d.ts +75 -75
  27. package/dist/index18.d.ts +40 -40
  28. package/dist/index19.d.ts +5 -5
  29. package/dist/index2.d.ts +4 -1
  30. package/dist/index21.d.ts +15 -15
  31. package/dist/index22.d.ts +8 -65
  32. package/dist/index23.d.ts +21 -13
  33. package/dist/index26.d.ts +4 -4
  34. package/dist/index27.d.ts +4 -4
  35. package/dist/index28.d.ts +10 -10
  36. package/dist/index3.d.ts +2 -2194
  37. package/dist/index31.d.ts +5 -5
  38. package/dist/index33.d.ts +3 -3
  39. package/dist/index34.d.ts +1 -1
  40. package/dist/index35.d.ts +1 -1
  41. package/dist/index36.d.ts +5 -5
  42. package/dist/index37.d.ts +8 -6
  43. package/dist/index38.d.ts +5 -5
  44. package/dist/index39.d.ts +1 -1
  45. package/dist/index4.d.ts +1356 -1356
  46. package/dist/index40.d.ts +2 -2
  47. package/dist/index8.d.ts +11 -3
  48. package/dist/index9.d.ts +2811 -3
  49. package/dist/inventory.d.ts +4 -4
  50. package/dist/inventory.js +3 -3
  51. package/dist/inventory.js.map +1 -1
  52. package/dist/marketplace2.js +3 -3
  53. package/dist/marketplace2.js.map +1 -1
  54. package/dist/metadata.d.ts +41 -41
  55. package/dist/primary-sale-checkout-options.d.ts +4 -4
  56. package/dist/quantityInput.js +1 -1
  57. package/dist/ranges.d.ts +12 -12
  58. package/dist/react/_internal/index.d.ts +1 -1
  59. package/dist/react/_internal/index.js +1 -1
  60. package/dist/react/index.d.ts +1 -1
  61. package/dist/react/queries/collectible/index.d.ts +1 -1
  62. package/dist/react/queries/index.d.ts +1 -1
  63. package/dist/react/ssr/index.d.ts +3 -3
  64. package/dist/react/ssr/index.js +3 -3
  65. package/dist/react/ui/components/marketplace-collectible-card/index.d.ts +1 -1
  66. package/dist/react/ui/modals/CreateListingModal/internal/hooks/index.d.ts +1 -1
  67. package/dist/react/ui/modals/MakeOfferModal/internal/hooks/index.d.ts +1 -1
  68. package/dist/react/ui/modals/_internal/components/alertMessage/index.d.ts +2 -2
  69. package/dist/react/ui/modals/_internal/components/baseModal/index.d.ts +6 -6
  70. package/dist/react/ui/modals/_internal/components/calendar/index.d.ts +2 -2
  71. package/dist/react/ui/modals/_internal/components/currencyImage/index.d.ts +2 -2
  72. package/dist/react/ui/modals/_internal/components/currencyOptionsSelect/index.d.ts +3 -3
  73. package/dist/react/ui/modals/_internal/components/floorPriceText/index.d.ts +2 -2
  74. package/dist/react/ui/modals/_internal/components/priceInput/index.d.ts +3 -5
  75. package/dist/react/ui/modals/_internal/components/quantityInput/index.d.ts +2 -2
  76. package/dist/react/ui/modals/_internal/components/selectWaasFeeOptions/index.d.ts +2 -2
  77. package/dist/react/ui/modals/_internal/components/switchChainErrorModal/index.d.ts +2 -2
  78. package/dist/react/ui/modals/_internal/components/timeAgo/index.d.ts +2 -2
  79. package/dist/react/ui/modals/_internal/components/tokenPreview/index.d.ts +3 -3
  80. package/dist/react/ui/modals/_internal/components/transaction-footer/index.d.ts +3 -3
  81. package/dist/react/ui/modals/_internal/components/transactionDetails/index.d.ts +3 -3
  82. package/dist/react/ui/modals/_internal/components/transactionPreview/index.d.ts +3 -3
  83. package/dist/react/ui/modals/_internal/components/transactionStatusModal/index.d.ts +3 -3
  84. package/dist/react.js +2279 -1919
  85. package/dist/react.js.map +1 -1
  86. package/dist/styles/index.css +15 -0
  87. package/dist/token-balances.d.ts +28 -28
  88. package/dist/transaction-footer.js +1 -1
  89. package/dist/types/index.d.ts +1 -1
  90. package/dist/types/index.js +1 -1
  91. package/dist/types.d.ts +1 -1
  92. package/dist/url-state.js +1 -1
  93. package/dist/utils/index.d.ts +2 -2
  94. package/dist/utils/index.js +2 -2
  95. package/dist/utils.js +31 -4
  96. package/dist/utils.js.map +1 -1
  97. package/package.json +7 -5
  98. package/src/react/hooks/config/useMarketplaceConfig.test.tsx +1 -0
  99. package/src/react/hooks/currency/list.test.tsx +23 -2
  100. package/src/react/hooks/transactions/useCancelTransactionSteps.tsx +4 -1
  101. package/src/react/hooks/transactions/useMarketTransactionSteps.tsx +55 -15
  102. package/src/react/hooks/utils/useEnsureCorrectChain.ts +10 -5
  103. package/src/react/queries/collectible/market-list.ts +5 -3
  104. package/src/react/queries/currency/list.ts +8 -5
  105. package/src/react/queries/inventory/inventory.ts +5 -3
  106. package/src/react/queries/marketplace/filters.ts +5 -3
  107. package/src/react/ui/modals/BuyModal/components/BuyModalContent.tsx +74 -37
  108. package/src/react/ui/modals/BuyModal/components/CryptoPaymentModal.tsx +74 -11
  109. package/src/react/ui/modals/BuyModal/components/Modal.tsx +62 -1
  110. package/src/react/ui/modals/BuyModal/hooks/useExecuteBundledTransactions.ts +13 -26
  111. package/src/react/ui/modals/BuyModal/hooks/useMarketPlatformFee.ts +5 -5
  112. package/src/react/ui/modals/BuyModal/internal/__tests__/buildTrailsMarketBuyActions.test.ts +213 -0
  113. package/src/react/ui/modals/BuyModal/internal/buildTrailsMarketBuyActions.ts +259 -0
  114. package/src/react/ui/modals/BuyModal/internal/buyModalContext.ts +79 -10
  115. package/src/react/ui/modals/BuyModal/internal/cryptoPaymentModalContext.tsx +44 -17
  116. package/src/react/ui/modals/CreateListingModal/internal/store.ts +5 -2
  117. package/src/react/ui/modals/MakeOfferModal/internal/context.ts +21 -1
  118. package/src/react/ui/modals/MakeOfferModal/internal/helpers/validation.ts +16 -1
  119. package/src/react/ui/modals/MakeOfferModal/internal/store.ts +5 -2
  120. package/src/react/ui/modals/SellModal/internal/store.ts +5 -2
  121. package/src/react/ui/modals/_internal/components/baseModal/errors/ModalInitializationError.tsx +8 -6
  122. package/src/react/ui/modals/_internal/components/currencyOptionsSelect/index.tsx +2 -1
  123. package/src/react/ui/modals/_internal/components/priceInput/index.tsx +13 -19
  124. package/src/react/ui/modals/_internal/components/transactionDetails/index.tsx +5 -2
  125. package/src/react/ui/modals/_internal/helpers/currency.test.ts +27 -0
  126. package/src/react/ui/modals/_internal/helpers/currency.ts +4 -2
  127. package/src/styles/styles.ts +18 -0
  128. package/src/utils/__tests__/getMarketplaceDetails.test.ts +10 -0
  129. package/src/utils/__tests__/getWebRPCErrorMessage.test.ts +28 -0
  130. package/src/utils/__tests__/marketplaceNormalization.test.ts +38 -0
  131. package/src/utils/collection.ts +19 -0
  132. package/src/utils/getConduitAddressForOrderbook.ts +2 -10
  133. package/src/utils/getMarketplaceDetails.ts +11 -4
  134. package/src/utils/getWebRPCErrorMessage.ts +21 -0
  135. package/src/utils/index.ts +1 -0
  136. package/src/utils/normalizeMarketplace.ts +31 -0
@@ -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: {
@@ -1,6 +1,7 @@
1
1
  import type { Address } from '@0xsequence/api-client';
2
2
  import { createStore } from '@xstate/store';
3
3
  import { useSelector } from '@xstate/store/react';
4
+ import { findMarketCollection } from '../../../../../utils';
4
5
  import { useMarketplaceConfig } from '../../../hooks';
5
6
 
6
7
  export type OpenCreateListingModalArgs = {
@@ -109,8 +110,10 @@ export const useCreateListingModalState = () => {
109
110
  } = useSelector(createListingModalStore, (state) => state.context);
110
111
 
111
112
  const { data: marketplaceConfig } = useMarketplaceConfig();
112
- const orderbookKind = marketplaceConfig?.market.collections.find(
113
- (collection) => collection.itemsAddress === collectionAddress,
113
+ const orderbookKind = findMarketCollection(
114
+ marketplaceConfig?.market.collections ?? [],
115
+ collectionAddress,
116
+ chainId,
114
117
  )?.destinationMarketplace;
115
118
 
116
119
  const closeModal = () => createListingModalStore.send({ type: 'close' });
@@ -5,12 +5,14 @@ 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,
11
12
  useCollectionMetadata,
12
13
  useConfig,
13
14
  useConnectorMetadata,
15
+ useCurrencyConvertToUSD,
14
16
  useCurrencyList,
15
17
  useTokenCurrencyBalance,
16
18
  } from '../../../../hooks';
@@ -106,6 +108,18 @@ export function useMakeOfferModalContext() {
106
108
  },
107
109
  });
108
110
 
111
+ const { data: usdConversion } = useCurrencyConvertToUSD({
112
+ chainId: state.chainId,
113
+ currencyAddress: selectedCurrency?.contractAddress ?? zeroAddress,
114
+ amountRaw: state.priceInput?.toString(),
115
+ query: {
116
+ enabled:
117
+ isOpenSeaOrderbook(state.orderbookKind) &&
118
+ !!selectedCurrency?.contractAddress &&
119
+ !!state.priceInput,
120
+ },
121
+ });
122
+
109
123
  const expiryDate = useMemo(
110
124
  () => new Date(Date.now() + state.expiryDays * 24 * 60 * 60 * 1000),
111
125
  [state.expiryDays],
@@ -139,6 +153,7 @@ export function useMakeOfferModalContext() {
139
153
  balance: balanceDnum,
140
154
  lowestListing: lowestListingDnum,
141
155
  orderbookKind: state.orderbookKind,
156
+ usdAmount: usdConversion?.usdAmount,
142
157
  });
143
158
 
144
159
  const formIsValid = isFormValid(validation);
@@ -359,6 +374,7 @@ export function useMakeOfferModalContext() {
359
374
  quantity: validation.quantity,
360
375
  balance: validation.balance,
361
376
  openseaCriteria: validation.openseaCriteria,
377
+ openseaMinPrice: validation.openseaMinPrice,
362
378
  },
363
379
  errors: {
364
380
  price: state.isPriceTouched ? validation.price.error : undefined,
@@ -369,6 +385,9 @@ export function useMakeOfferModalContext() {
369
385
  openseaCriteria: state.isPriceTouched
370
386
  ? validation.openseaCriteria?.error
371
387
  : undefined,
388
+ openseaMinPrice: state.isPriceTouched
389
+ ? validation.openseaMinPrice?.error
390
+ : undefined,
372
391
  },
373
392
  },
374
393
 
@@ -415,7 +434,8 @@ export function useMakeOfferModalContext() {
415
434
  this.form.errors.price ||
416
435
  this.form.errors.quantity ||
417
436
  this.form.errors.balance ||
418
- this.form.errors.openseaCriteria
437
+ this.form.errors.openseaCriteria ||
438
+ this.form.errors.openseaMinPrice
419
439
  );
420
440
  },
421
441
 
@@ -11,6 +11,7 @@ export type OfferValidation = {
11
11
  quantity: FieldValidation;
12
12
  balance: FieldValidation;
13
13
  openseaCriteria?: FieldValidation;
14
+ openseaMinPrice?: FieldValidation;
14
15
  };
15
16
 
16
17
  export function validateOfferForm({
@@ -19,12 +20,14 @@ export function validateOfferForm({
19
20
  balance,
20
21
  lowestListing,
21
22
  orderbookKind,
23
+ usdAmount,
22
24
  }: {
23
25
  price: Dnum;
24
26
  quantity: Dnum;
25
27
  balance?: Dnum;
26
28
  lowestListing?: Dnum;
27
29
  orderbookKind?: string;
30
+ usdAmount?: number;
28
31
  }): OfferValidation {
29
32
  const validation: OfferValidation = {
30
33
  price: { isValid: true, error: null },
@@ -67,6 +70,15 @@ export function validateOfferForm({
67
70
  };
68
71
  }
69
72
 
73
+ // OpenSea minimum price validation ($0.01 USD)
74
+ if (orderbookKind === 'opensea' && usdAmount !== undefined) {
75
+ const meetsMinPrice = usdAmount >= 0.01;
76
+ validation.openseaMinPrice = {
77
+ isValid: meetsMinPrice,
78
+ error: meetsMinPrice ? null : 'Lowest price must be at least $0.01',
79
+ };
80
+ }
81
+
70
82
  return validation;
71
83
  }
72
84
 
@@ -75,7 +87,8 @@ export function isFormValid(validation: OfferValidation): boolean {
75
87
  validation.price.isValid &&
76
88
  validation.quantity.isValid &&
77
89
  validation.balance.isValid &&
78
- (validation.openseaCriteria?.isValid ?? true)
90
+ (validation.openseaCriteria?.isValid ?? true) &&
91
+ (validation.openseaMinPrice?.isValid ?? true)
79
92
  );
80
93
  }
81
94
 
@@ -87,6 +100,8 @@ export function getValidationErrors(validation: OfferValidation): string[] {
87
100
  if (validation.balance.error) errors.push(validation.balance.error);
88
101
  if (validation.openseaCriteria?.error)
89
102
  errors.push(validation.openseaCriteria.error);
103
+ if (validation.openseaMinPrice?.error)
104
+ errors.push(validation.openseaMinPrice.error);
90
105
 
91
106
  return errors;
92
107
  }
@@ -1,6 +1,7 @@
1
1
  import type { Address } from '@0xsequence/api-client';
2
2
  import { createStore } from '@xstate/store';
3
3
  import { useSelector } from '@xstate/store/react';
4
+ import { findMarketCollection } from '../../../../../utils';
4
5
  import { useMarketplaceConfig } from '../../../hooks';
5
6
 
6
7
  export type OpenMakeOfferModalArgs = {
@@ -109,8 +110,10 @@ export const useMakeOfferModalState = () => {
109
110
  } = useSelector(makeOfferModalStore, (state) => state.context);
110
111
 
111
112
  const { data: marketplaceConfig } = useMarketplaceConfig();
112
- const orderbookKind = marketplaceConfig?.market.collections.find(
113
- (collection) => collection.itemsAddress === collectionAddress,
113
+ const orderbookKind = findMarketCollection(
114
+ marketplaceConfig?.market.collections ?? [],
115
+ collectionAddress,
116
+ chainId,
114
117
  )?.destinationMarketplace;
115
118
 
116
119
  const closeModal = () => makeOfferModalStore.send({ type: 'close' });
@@ -2,6 +2,7 @@ import type { Address } from '@0xsequence/api-client';
2
2
  import { createStore } from '@xstate/store';
3
3
  import { useSelector } from '@xstate/store/react';
4
4
  import { zeroAddress } from 'viem';
5
+ import { findMarketCollection } from '../../../../../utils';
5
6
  import type { Order } from '../../../../_internal';
6
7
  import { useMarketplaceConfig } from '../../../hooks';
7
8
 
@@ -56,8 +57,10 @@ export const useSellModalState = () => {
56
57
  );
57
58
 
58
59
  const { data: marketplaceConfig } = useMarketplaceConfig();
59
- const orderbookKind = marketplaceConfig?.market.collections.find(
60
- (collection) => collection.itemsAddress === collectionAddress,
60
+ const orderbookKind = findMarketCollection(
61
+ marketplaceConfig?.market.collections ?? [],
62
+ collectionAddress,
63
+ chainId,
61
64
  )?.destinationMarketplace;
62
65
 
63
66
  const closeModal = () => sellModalStore.send({ type: 'close' });
@@ -17,7 +17,7 @@ export const ModalInitializationError = ({
17
17
  const [showTechnicalDetails, setShowTechnicalDetails] = useState(false);
18
18
 
19
19
  return (
20
- <div className="flex min-h-[400px] flex-col items-center justify-center p-4 text-white">
20
+ <div className="flex min-h-[400px] flex-col items-center justify-center p-6 text-white">
21
21
  <div className="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-red-500">
22
22
  <WarningIcon className="h-8 w-8 text-white" />
23
23
  </div>
@@ -29,7 +29,7 @@ export const ModalInitializationError = ({
29
29
  refresh the page.
30
30
  </p>
31
31
 
32
- {error.stack && (
32
+ {(error.cause as string) && (
33
33
  <button
34
34
  type="button"
35
35
  onClick={() => setShowTechnicalDetails(!showTechnicalDetails)}
@@ -50,19 +50,21 @@ export const ModalInitializationError = ({
50
50
  d="M19 9l-7 7-7-7"
51
51
  />
52
52
  </svg>
53
- Show technical details
53
+ {showTechnicalDetails
54
+ ? 'Hide technical details'
55
+ : 'Show technical details'}
54
56
  </button>
55
57
  )}
56
58
 
57
- {showTechnicalDetails && error.stack && (
59
+ {showTechnicalDetails && (error.cause as string) && (
58
60
  <div className="mb-8 max-h-64 w-full max-w-md overflow-y-auto rounded-lg border border-red-900 bg-[#2b0000] p-4">
59
61
  <pre className="whitespace-pre-wrap break-words text-red-100 text-xs">
60
- {error.stack}
62
+ {error.cause as string}
61
63
  </pre>
62
64
  </div>
63
65
  )}
64
66
 
65
- <div className="flex w-full max-w-xs flex-col space-y-3">
67
+ <div className="flex w-full flex-col space-y-3">
66
68
  {onTryAgain && (
67
69
  <Button
68
70
  onClick={onTryAgain}
@@ -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,
@@ -36,7 +37,6 @@ type PriceInputProps = {
36
37
  };
37
38
  disabled?: boolean;
38
39
  orderbookKind?: OrderbookKind;
39
- setOpenseaLowestPriceCriteriaMet?: (state: boolean) => void;
40
40
  modalType?: 'listing' | 'offer';
41
41
  // Fee data for enhanced balance checking in offers
42
42
  feeData?: {
@@ -55,7 +55,6 @@ export default function PriceInput({
55
55
  includeNativeCurrency,
56
56
  disabled,
57
57
  orderbookKind,
58
- setOpenseaLowestPriceCriteriaMet,
59
58
  modalType,
60
59
  feeData,
61
60
  }: PriceInputProps) {
@@ -76,14 +75,13 @@ export default function PriceInput({
76
75
  useConvertPriceToUSD({
77
76
  chainId,
78
77
  currencyAddress: currencyAddress ?? zeroAddress,
79
- amountRaw: priceAmountRaw?.toString(),
80
- query: {
81
- enabled:
82
- orderbookKind === OrderbookKind.opensea &&
83
- !!currencyAddress &&
84
- !!priceAmountRaw &&
85
- !!setOpenseaLowestPriceCriteriaMet,
86
- },
78
+ amountRaw: priceAmountRaw?.toString(),
79
+ query: {
80
+ enabled:
81
+ isOpenSeaOrderbook(orderbookKind) &&
82
+ !!currencyAddress &&
83
+ !!priceAmountRaw,
84
+ },
87
85
  });
88
86
 
89
87
  useEffect(() => {
@@ -163,18 +161,14 @@ export default function PriceInput({
163
161
  );
164
162
 
165
163
  const openseaLowestPriceCriteriaMet =
166
- orderbookKind === OrderbookKind.opensea &&
167
- !!conversion?.usdAmount &&
164
+ isOpenSeaOrderbook(orderbookKind) &&
165
+ conversion?.usdAmount !== undefined &&
168
166
  conversion.usdAmount >= 0.01;
169
167
 
170
168
  if (checkBalance?.enabled) {
171
169
  checkBalance.callback(balanceError);
172
170
  }
173
171
 
174
- if (setOpenseaLowestPriceCriteriaMet) {
175
- setOpenseaLowestPriceCriteriaMet(openseaLowestPriceCriteriaMet);
176
- }
177
-
178
172
  const [value, setValue] = useState('0');
179
173
  const prevCurrencyDecimals = useRef(currencyDecimals);
180
174
  const [openseaDecimalError, setOpenseaDecimalError] = useState<string | null>(
@@ -212,7 +206,7 @@ export default function PriceInput({
212
206
  if (!price || !onPriceChange) return;
213
207
 
214
208
  // Validate OpenSea decimal constraints for offers
215
- if (orderbookKind === OrderbookKind.opensea && modalType === 'offer') {
209
+ if (isOpenSeaOrderbook(orderbookKind) && modalType === 'offer') {
216
210
  const validation = validateOpenseaOfferDecimals(newValue);
217
211
  if (!validation.isValid) {
218
212
  setOpenseaDecimalError(validation.errorMessage || null);
@@ -325,7 +319,7 @@ export default function PriceInput({
325
319
  {!balanceError &&
326
320
  priceAmountRaw !== 0n &&
327
321
  !openseaLowestPriceCriteriaMet &&
328
- orderbookKind === OrderbookKind.opensea &&
322
+ isOpenSeaOrderbook(orderbookKind) &&
329
323
  !isConversionLoading &&
330
324
  modalType === 'offer' &&
331
325
  !openseaDecimalError && (
@@ -339,7 +333,7 @@ export default function PriceInput({
339
333
 
340
334
  {!balanceError &&
341
335
  openseaDecimalError &&
342
- orderbookKind === OrderbookKind.opensea &&
336
+ isOpenSeaOrderbook(orderbookKind) &&
343
337
  modalType === 'offer' && (
344
338
  <Text className="font-body font-medium text-xs" color="negative">
345
339
  {openseaDecimalError}
@@ -11,6 +11,7 @@ import {
11
11
  import { useEffect, useState } from 'react';
12
12
  import { DEFAULT_MARKETPLACE_FEE_PERCENTAGE } from '../../../../../../consts';
13
13
  import type { Price } from '../../../../../../types';
14
+ import { findMarketCollection } from '../../../../../../utils';
14
15
  import { calculateEarningsAfterFees } from '../../../../../../utils/price';
15
16
  import { useMarketplaceConfig, useRoyalty } from '../../../../../hooks';
16
17
 
@@ -37,8 +38,10 @@ export default function TransactionDetails({
37
38
  const { data, isLoading: marketplaceConfigLoading } = useMarketplaceConfig();
38
39
 
39
40
  const marketplaceFeePercentage = includeMarketplaceFee
40
- ? data?.market.collections.find(
41
- (collection) => collection.itemsAddress === collectionAddress,
41
+ ? findMarketCollection(
42
+ data?.market.collections ?? [],
43
+ collectionAddress,
44
+ chainId,
42
45
  )?.feePercentage || DEFAULT_MARKETPLACE_FEE_PERCENTAGE
43
46
  : 0;
44
47
  // royaltyPercentage is an array of [recipient, percentage]
@@ -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
+ });
@@ -1,6 +1,7 @@
1
1
  import { zeroAddress } from 'viem';
2
2
  import { OrderbookKind } from '../../../../../types';
3
3
  import { compareAddress } from '../../../../../utils';
4
+ import { isOpenSeaOrderbook, normalizeOrderbookKind } from '../../../../../utils/normalizeMarketplace';
4
5
  import type { Currency } from '../../../../_internal';
5
6
  import { getOpenseaCurrencyForChain } from '../../_internal/constants/opensea-currencies';
6
7
 
@@ -12,7 +13,7 @@ export function filterCurrenciesForOrderbook(
12
13
  ): Currency[] {
13
14
  if (!currencies || currencies.length === 0) return [];
14
15
 
15
- if (orderbookKind === OrderbookKind.opensea) {
16
+ if (isOpenSeaOrderbook(orderbookKind)) {
16
17
  const openseaCurrency = getOpenseaCurrencyForChain(chainId, side);
17
18
  if (openseaCurrency) {
18
19
  return currencies.filter((currency) =>
@@ -35,8 +36,9 @@ export function getDefaultCurrency(
35
36
  return currencies[0];
36
37
  }
37
38
 
39
+ const normalizedOrderbookKind = normalizeOrderbookKind(orderbookKind);
38
40
  const shouldSkipNative =
39
- orderbookKind !== OrderbookKind.sequence_marketplace_v2 &&
41
+ normalizedOrderbookKind !== OrderbookKind.sequence_marketplace_v2 &&
40
42
  currencies.length > 1;
41
43
 
42
44
  return shouldSkipNative ? currencies[1] : currencies[0];
@@ -1855,6 +1855,17 @@ export const styles = String.raw`/* Modified Tailwind CSS, to avoid issues with
1855
1855
  --tw-tracking: var(--tracking-normal);
1856
1856
  letter-spacing: var(--tracking-normal);
1857
1857
  }
1858
+ .text-small {
1859
+ font-family: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
1860
+ font-size: var(--text-xs);
1861
+ line-height: var(--tw-leading, var(--text-xs--line-height));
1862
+ --tw-leading: calc(var(--spacing) * 4);
1863
+ line-height: calc(var(--spacing) * 4);
1864
+ --tw-font-weight: var(--font-weight-medium);
1865
+ font-weight: var(--font-weight-medium);
1866
+ --tw-tracking: var(--tracking-wide);
1867
+ letter-spacing: var(--tracking-wide);
1868
+ }
1858
1869
  .font-body {
1859
1870
  font-family: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
1860
1871
  }
@@ -1985,6 +1996,9 @@ export const styles = String.raw`/* Modified Tailwind CSS, to avoid issues with
1985
1996
  .break-words {
1986
1997
  overflow-wrap: break-word;
1987
1998
  }
1999
+ .wrap-anywhere {
2000
+ overflow-wrap: anywhere;
2001
+ }
1988
2002
  .text-ellipsis {
1989
2003
  text-overflow: ellipsis;
1990
2004
  }
@@ -2313,6 +2327,10 @@ export const styles = String.raw`/* Modified Tailwind CSS, to avoid issues with
2313
2327
  --tw-duration: 100ms;
2314
2328
  transition-duration: 100ms;
2315
2329
  }
2330
+ .duration-150 {
2331
+ --tw-duration: 150ms;
2332
+ transition-duration: 150ms;
2333
+ }
2316
2334
  .duration-200 {
2317
2335
  --tw-duration: 200ms;
2318
2336
  transition-duration: 200ms;
@@ -38,6 +38,16 @@ describe('getMarketplaceDetails', () => {
38
38
  expect(result?.logo).toBe(OpenSeaLogo);
39
39
  expect(result?.displayName).toBe('OpenSea');
40
40
  });
41
+
42
+ test('should map Magic Eden kind to OpenSea', () => {
43
+ const result = getMarketplaceDetails({
44
+ originName: 'Magic Eden',
45
+ kind: MarketplaceKind.magic_eden,
46
+ });
47
+ expect(result).toBeDefined();
48
+ expect(result?.logo).toBe(OpenSeaLogo);
49
+ expect(result?.displayName).toBe('OpenSea');
50
+ });
41
51
  });
42
52
 
43
53
  describe('detection by marketplace name', () => {
@@ -0,0 +1,28 @@
1
+ import type { WebrpcError } from '@0xsequence/api-client';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { getWebRPCErrorMessage } from '../getWebRPCErrorMessage';
4
+
5
+ describe('getWebRPCErrorMessage', () => {
6
+ test('maps OutOfFund endpoint errors to an insufficient balance message', () => {
7
+ const message = getWebRPCErrorMessage({
8
+ code: 0,
9
+ status: 400,
10
+ name: 'WebrpcEndpoint',
11
+ message: 'endpoint error',
12
+ cause: 'evm error: OutOfFund',
13
+ } as WebrpcError);
14
+
15
+ expect(message).toBe('Insufficient balance to complete this transaction.');
16
+ });
17
+
18
+ test('continues to map known WebRPC error codes', () => {
19
+ const message = getWebRPCErrorMessage({
20
+ code: 3000,
21
+ status: 404,
22
+ name: 'WebrpcNotFound',
23
+ message: 'not found',
24
+ } as WebrpcError);
25
+
26
+ expect(message).toBe('Item not found or no longer available.');
27
+ });
28
+ });