@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
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { Button, Spinner, Text } from '@0xsequence/design-system';
4
- import { useState } from 'react';
4
+ import { useEffect, useState } from 'react';
5
5
  import type { Hex } from 'viem';
6
6
  import { getPresentableChainName } from '../../../../../utils/network';
7
7
  import type { Step } from '../../../../_internal';
@@ -39,13 +39,36 @@ export const CryptoPaymentModal = ({
39
39
  } = useCryptoPaymentModalContext({ chainId, steps, onSuccess });
40
40
 
41
41
  const { ensureCorrectChainAsync } = useEnsureCorrectChain();
42
+ const [pendingAction, setPendingAction] = useState<'approval' | 'buy' | null>(
43
+ null,
44
+ );
42
45
  const [chainSwitchError, setChainSwitchError] = useState<{
43
46
  title: string;
44
47
  message: string;
45
48
  details?: Error;
46
49
  } | null>(null);
50
+ const isPendingApprovalReady =
51
+ pendingAction === 'approval' && isOnCorrectChain && canApprove;
52
+ const isPendingBuyReady = pendingAction === 'buy' && isOnCorrectChain && canBuy;
53
+ const isPendingAction = pendingAction !== null;
54
+
55
+ useEffect(() => {
56
+ if (!isPendingApprovalReady && !isPendingBuyReady) {
57
+ return;
58
+ }
59
+
60
+ setPendingAction(null);
61
+
62
+ void (pendingAction === 'approval' ? executeApproval() : executeBuy());
63
+ }, [
64
+ executeApproval,
65
+ executeBuy,
66
+ isPendingApprovalReady,
67
+ isPendingBuyReady,
68
+ pendingAction,
69
+ ]);
47
70
 
48
- const handleChainSwitchError = (error: Error) => {
71
+ const handleChainSwitchError = (error?: Error) => {
49
72
  const chainName = getPresentableChainName(chainId);
50
73
  setChainSwitchError({
51
74
  title: 'Chain switch failed',
@@ -58,27 +81,59 @@ export const CryptoPaymentModal = ({
58
81
  setChainSwitchError(null);
59
82
  };
60
83
 
84
+ const executeAction = async (action: 'approval' | 'buy') => {
85
+ setPendingAction(null);
86
+
87
+ if (action === 'approval') {
88
+ await executeApproval();
89
+ return;
90
+ }
91
+
92
+ await executeBuy();
93
+ };
94
+
61
95
  const executeWithChainSwitch = async (action: 'approval' | 'buy') => {
62
96
  dismissChainSwitchError();
97
+
98
+ if (isOnCorrectChain) {
99
+ await executeAction(action);
100
+ return;
101
+ }
102
+
103
+ setPendingAction(action);
104
+
63
105
  try {
64
- await ensureCorrectChainAsync(chainId);
106
+ const didRequestChainSwitch = await ensureCorrectChainAsync(chainId);
107
+ if (!didRequestChainSwitch) {
108
+ setPendingAction(null);
109
+ handleChainSwitchError();
110
+ }
65
111
  } catch (error) {
112
+ setPendingAction(null);
113
+
66
114
  if (error instanceof Error) {
67
115
  handleChainSwitchError(error);
116
+ } else {
117
+ handleChainSwitchError();
68
118
  }
69
119
  }
70
-
71
- if (action === 'approval') {
72
- await executeApproval();
73
- } else {
74
- await executeBuy();
75
- }
76
120
  };
77
121
 
122
+ const isWaitingForChainSwitch = isPendingAction && !isOnCorrectChain;
123
+ const isPreparingPendingAction = isPendingAction && isOnCorrectChain;
124
+
78
125
  const approvalButtonLabel = isApproving ? (
79
126
  <div className="flex items-center gap-2">
80
127
  <Spinner size="sm" /> Approving Token...
81
128
  </div>
129
+ ) : isWaitingForChainSwitch && pendingAction === 'approval' ? (
130
+ <div className="flex items-center gap-2">
131
+ <Spinner size="sm" /> Switching Network...
132
+ </div>
133
+ ) : isPreparingPendingAction && pendingAction === 'approval' ? (
134
+ <div className="flex items-center gap-2">
135
+ <Spinner size="sm" /> Preparing Approval...
136
+ </div>
82
137
  ) : (
83
138
  'Approve Token'
84
139
  );
@@ -87,6 +142,14 @@ export const CryptoPaymentModal = ({
87
142
  <div className="flex items-center gap-2">
88
143
  <Spinner size="sm" /> Confirming Purchase...
89
144
  </div>
145
+ ) : isWaitingForChainSwitch && pendingAction === 'buy' ? (
146
+ <div className="flex items-center gap-2">
147
+ <Spinner size="sm" /> Switching Network...
148
+ </div>
149
+ ) : isPreparingPendingAction && pendingAction === 'buy' ? (
150
+ <div className="flex items-center gap-2">
151
+ <Spinner size="sm" /> Preparing Purchase...
152
+ </div>
90
153
  ) : (
91
154
  'Buy now'
92
155
  );
@@ -129,7 +192,7 @@ export const CryptoPaymentModal = ({
129
192
  onClick={async () => {
130
193
  await executeWithChainSwitch('approval');
131
194
  }}
132
- disabled={!canApprove}
195
+ disabled={!canApprove || isPendingAction}
133
196
  variant="primary"
134
197
  size="lg"
135
198
  className="w-full"
@@ -143,7 +206,7 @@ export const CryptoPaymentModal = ({
143
206
  onClick={async () => {
144
207
  await executeWithChainSwitch('buy');
145
208
  }}
146
- disabled={!canBuy}
209
+ disabled={!canBuy || isPendingAction}
147
210
  variant="primary"
148
211
  size="lg"
149
212
  className="w-full"
@@ -1,8 +1,26 @@
1
1
  'use client';
2
2
 
3
+ import {
4
+ type WagmiAdapterOptions,
5
+ wagmiAdapter,
6
+ } from '@0xtrails/adapter-wagmi';
7
+ import { TrailsProvider } from '0xtrails/widget';
8
+ import { useConfig as useWagmiConfig } from 'wagmi';
9
+ import {
10
+ getSequenceApiUrl,
11
+ getSequenceIndexerUrl,
12
+ getSequenceNodeGatewayUrl,
13
+ getTrailsApiUrl,
14
+ } from '../../../../_internal/api/services';
15
+ import { useConfig } from '../../../../hooks';
16
+ import { useWaasFeeOptions } from '../../../../hooks/utils/useWaasFeeOptions';
3
17
  import { useIsOpen } from '../store';
4
18
  import { BuyModalContent } from './BuyModalContent';
5
19
 
20
+ type TrailsUseWaasFeeOptions = NonNullable<
21
+ NonNullable<WagmiAdapterOptions['sequence']>['useWaasFeeOptions']
22
+ >;
23
+
6
24
  export const BuyModal = () => {
7
25
  const isOpen = useIsOpen();
8
26
 
@@ -10,5 +28,48 @@ export const BuyModal = () => {
10
28
  return null;
11
29
  }
12
30
 
13
- return <BuyModalContent />;
31
+ return <BuyModalWithTrailsProvider />;
32
+ };
33
+
34
+ const useTrailsWaasFeeOptions: TrailsUseWaasFeeOptions = ({
35
+ chainIdOverride,
36
+ } = {}) => {
37
+ const config = useConfig();
38
+ const {
39
+ pendingFeeOptionConfirmation,
40
+ confirmPendingFeeOption,
41
+ rejectPendingFeeOption,
42
+ } = useWaasFeeOptions(chainIdOverride ?? 0, config);
43
+
44
+ return [
45
+ pendingFeeOptionConfirmation,
46
+ confirmPendingFeeOption,
47
+ rejectPendingFeeOption,
48
+ ];
49
+ };
50
+
51
+ const BuyModalWithTrailsProvider = () => {
52
+ const config = useConfig();
53
+ const wagmiConfig = useWagmiConfig();
54
+
55
+ return (
56
+ <TrailsProvider
57
+ config={{
58
+ trailsApiKey: config.projectAccessKey,
59
+ trailsApiUrl: getTrailsApiUrl(config),
60
+ sequenceIndexerUrl: getSequenceIndexerUrl(config),
61
+ sequenceNodeGatewayUrl: getSequenceNodeGatewayUrl(config),
62
+ sequenceApiUrl: getSequenceApiUrl(config),
63
+ walletConnectProjectId: config.walletConnectProjectId,
64
+ adapters: [
65
+ wagmiAdapter({
66
+ wagmiConfig,
67
+ sequence: { useWaasFeeOptions: useTrailsWaasFeeOptions },
68
+ }),
69
+ ],
70
+ }}
71
+ >
72
+ <BuyModalContent />
73
+ </TrailsProvider>
74
+ );
14
75
  };
@@ -5,7 +5,6 @@ import type { Hex } from 'viem';
5
5
  import { useAccount, usePublicClient, useWalletClient } from 'wagmi';
6
6
  import { useConfig } from '../../../..';
7
7
  import { getIndexerClient, type Step } from '../../../../_internal';
8
- import { useBuyModalData } from './useBuyModalData';
9
8
 
10
9
  // https://github.com/0xsequence/web-sdk/blob/620b6fe7681ae49efd4eb3fa7607ef01dd7ede54/packages/connect/src/utils/transactions.ts#L11-L19
11
10
  class FeeOptionInsufficientFundsError extends Error {
@@ -21,31 +20,25 @@ class FeeOptionInsufficientFundsError extends Error {
21
20
  type UseExecuteBundledTransactions = {
22
21
  chainId: number;
23
22
  approvalStep?: Step;
24
- priceAmount: bigint;
25
23
  };
26
24
 
27
25
  const useExecuteBundledTransactions = ({
28
26
  chainId,
29
27
  approvalStep,
30
- priceAmount,
31
28
  }: UseExecuteBundledTransactions) => {
32
29
  const config = useConfig();
33
30
  const [isExecuting, setIsExecuting] = useState(false);
34
31
  const { address, connector } = useAccount();
35
- const publicClient = usePublicClient();
32
+ const publicClient = usePublicClient({ chainId });
36
33
  const { data: walletClient } = useWalletClient();
37
34
  const indexerClient = getIndexerClient(chainId, config);
38
35
 
39
- const { collection, currency } = useBuyModalData();
40
-
41
36
  const isReady =
42
37
  !!address &&
43
38
  !!publicClient &&
44
39
  !!walletClient &&
45
40
  !!indexerClient &&
46
- !!connector &&
47
- !!collection?.address &&
48
- priceAmount != null;
41
+ !!connector;
49
42
 
50
43
  const executeBundledTransactions = async ({
51
44
  step,
@@ -79,28 +72,22 @@ const useExecuteBundledTransactions = ({
79
72
  throw new Error('Connector not found');
80
73
  }
81
74
 
82
- if (!collection?.address) {
83
- throw new Error('Collection address not found');
84
- }
75
+ const buildTransaction = (step: Step) => {
76
+ const value = step.value ? BigInt(step.value) : 0n;
85
77
 
86
- if (priceAmount == null) {
87
- throw new Error('Price amount not found');
88
- }
78
+ return {
79
+ to: step.to,
80
+ data: step.data as Hex,
81
+ chainId,
82
+ ...(value > 0n ? { value } : {}),
83
+ };
84
+ };
89
85
 
90
86
  const approvalData = approvalStep
91
- ? {
92
- to: approvalStep.to,
93
- data: approvalStep.data as Hex,
94
- chainId,
95
- }
87
+ ? buildTransaction(approvalStep)
96
88
  : undefined;
97
89
 
98
- const transactionData = {
99
- to: step.to,
100
- data: step.data as Hex,
101
- chainId,
102
- ...(currency?.nativeCurrency ? { value: priceAmount } : {}),
103
- };
90
+ const transactionData = buildTransaction(step);
104
91
 
105
92
  const transactions = [
106
93
  ...(approvalData ? [approvalData] : []),
@@ -0,0 +1,213 @@
1
+ import {
2
+ type BuyStep,
3
+ ContractType,
4
+ MarketplaceKind,
5
+ type Order,
6
+ StepType,
7
+ } from '@0xsequence/api-client';
8
+ import { encodeDestinationCalls } from '0xtrails';
9
+ import { type Address, decodeFunctionData, erc20Abi, zeroAddress } from 'viem';
10
+ import { describe, expect, it } from 'vitest';
11
+ import { OPENSEA_SEAPORT_CONDUIT_ADDRESS } from '../../../../../../utils/getConduitAddressForOrderbook';
12
+ import { buildTrailsMarketBuyActions } from '../buildTrailsMarketBuyActions';
13
+
14
+ const BUY_TARGET = '0x1000000000000000000000000000000000000001' as Address;
15
+ const USER_WALLET = '0x2000000000000000000000000000000000000002' as Address;
16
+ const ERC20_CURRENCY = '0x3000000000000000000000000000000000000003' as Address;
17
+ const COLLECTION_ADDRESS =
18
+ '0x4000000000000000000000000000000000000004' as Address;
19
+ const SEAPORT_ADDRESS = '0x0000000000000068F116a894984e2DB1123eB395' as Address;
20
+ const BASE_WETH = '0x4200000000000000000000000000000000000006' as Address;
21
+ const OPENSEA_FULFILL_BASIC_ORDER_SELECTOR = '0xfb0f3ee1';
22
+
23
+ const createBuyStep = (overrides: Partial<BuyStep> = {}): BuyStep =>
24
+ ({
25
+ id: StepType.buy,
26
+ to: BUY_TARGET,
27
+ data: '0xabcdef',
28
+ value: 0n,
29
+ price: 100n,
30
+ ...overrides,
31
+ }) as BuyStep;
32
+
33
+ const createOrder = (overrides: Partial<Order> = {}): Order =>
34
+ ({
35
+ orderId: 'order-1',
36
+ marketplace: MarketplaceKind.sequence_marketplace_v2,
37
+ collectionContractAddress: COLLECTION_ADDRESS,
38
+ tokenId: 7n,
39
+ priceCurrencyAddress: ERC20_CURRENCY,
40
+ priceAmount: 100n,
41
+ ...overrides,
42
+ }) as Order;
43
+
44
+ const build = (
45
+ overrides: {
46
+ buyStep?: Partial<BuyStep>;
47
+ marketOrder?: Partial<Order>;
48
+ contractType?: ContractType.ERC721 | ContractType.ERC1155;
49
+ } = {},
50
+ ) =>
51
+ buildTrailsMarketBuyActions({
52
+ chainId: 8453,
53
+ buyStep: createBuyStep(overrides.buyStep),
54
+ marketOrder: createOrder(overrides.marketOrder),
55
+ contractType: overrides.contractType ?? ContractType.ERC721,
56
+ recipientAddress: USER_WALLET,
57
+ });
58
+
59
+ describe('buildTrailsMarketBuyActions', () => {
60
+ it('builds ERC20 approval and marketplace buy calls using the required payment amount', () => {
61
+ const result = build();
62
+
63
+ expect(result).toBeDefined();
64
+ expect(result?.paymentTokenAddress).toBe(ERC20_CURRENCY);
65
+ expect(result?.paymentAmount).toBe(100n);
66
+ expect(result?.calls).toHaveLength(2);
67
+ expect(result?.calls[0]).toMatchObject({
68
+ to: ERC20_CURRENCY,
69
+ });
70
+ expect(result?.calls[0]?.data).toMatch(/^0x095ea7b3/);
71
+ const approval = decodeFunctionData({
72
+ abi: erc20Abi,
73
+ data: result?.calls[0]?.data ?? '0x',
74
+ });
75
+ expect(approval.args[1]).toBe(100n);
76
+
77
+ expect(result?.calls[1]).toMatchObject({
78
+ to: BUY_TARGET,
79
+ data: '0xabcdef',
80
+ });
81
+ expect(result?.calls[1]?.value).toBe(undefined);
82
+
83
+ expect(() =>
84
+ encodeDestinationCalls({
85
+ calls: result?.calls ?? [],
86
+ tokenAddress: ERC20_CURRENCY,
87
+ sweepTarget: USER_WALLET,
88
+ }),
89
+ ).not.toThrow();
90
+ });
91
+
92
+ it('encodes OpenSea ERC20 buys even when approval spender is the OpenSea conduit', () => {
93
+ const result = build({
94
+ buyStep: {
95
+ to: SEAPORT_ADDRESS,
96
+ data: OPENSEA_FULFILL_BASIC_ORDER_SELECTOR,
97
+ },
98
+ marketOrder: {
99
+ marketplace: MarketplaceKind.opensea,
100
+ },
101
+ });
102
+
103
+ expect(result).toBeDefined();
104
+ expect(result?.calls).toHaveLength(3);
105
+ const approval = decodeFunctionData({
106
+ abi: erc20Abi,
107
+ data: result?.calls[0]?.data ?? '0x',
108
+ });
109
+ expect(approval.args[0]).toBe(OPENSEA_SEAPORT_CONDUIT_ADDRESS);
110
+ expect(approval.args[1]).toBe(100n);
111
+ expect(result?.calls[1]).toMatchObject({
112
+ to: SEAPORT_ADDRESS,
113
+ data: OPENSEA_FULFILL_BASIC_ORDER_SELECTOR,
114
+ });
115
+ expect(result?.calls[2]?.to).toBe(COLLECTION_ADDRESS);
116
+ expect(result?.calls[2]?.data).toMatch(/^0x23b872dd/);
117
+
118
+ expect(() =>
119
+ encodeDestinationCalls({
120
+ calls: result?.calls ?? [],
121
+ tokenAddress: result?.paymentTokenAddress ?? ERC20_CURRENCY,
122
+ sweepTarget: USER_WALLET,
123
+ }),
124
+ ).not.toThrow();
125
+ });
126
+
127
+ it('uses wrapped native currency for native marketplace buys', () => {
128
+ const result = build({
129
+ buyStep: {
130
+ value: 123n,
131
+ price: 0n,
132
+ },
133
+ marketOrder: {
134
+ priceCurrencyAddress: zeroAddress,
135
+ priceAmount: 123n,
136
+ },
137
+ });
138
+
139
+ expect(result).toBeDefined();
140
+ expect(result?.paymentTokenAddress).toBe(BASE_WETH);
141
+ expect(result?.paymentAmount).toBe(123n);
142
+ expect(result?.calls).toHaveLength(2);
143
+ expect(result?.calls[0]).toMatchObject({
144
+ to: BASE_WETH,
145
+ });
146
+ expect(result?.calls[0]?.data).toMatch(/^0x2e1a7d4d/);
147
+ expect(result?.calls[1]).toMatchObject({
148
+ to: BUY_TARGET,
149
+ data: '0xabcdef',
150
+ value: 123n,
151
+ sweepTokens: [zeroAddress],
152
+ });
153
+
154
+ expect(() =>
155
+ encodeDestinationCalls({
156
+ calls: result?.calls ?? [],
157
+ tokenAddress: result?.paymentTokenAddress ?? BASE_WETH,
158
+ sweepTarget: USER_WALLET,
159
+ }),
160
+ ).not.toThrow();
161
+ });
162
+
163
+ it('wraps native OpenSea buys through WETH and appends an NFT transfer', () => {
164
+ const result = build({
165
+ buyStep: {
166
+ to: SEAPORT_ADDRESS,
167
+ data: OPENSEA_FULFILL_BASIC_ORDER_SELECTOR,
168
+ value: 123n,
169
+ price: 0n,
170
+ },
171
+ marketOrder: {
172
+ marketplace: MarketplaceKind.opensea,
173
+ priceCurrencyAddress: zeroAddress,
174
+ priceAmount: 123n,
175
+ },
176
+ });
177
+
178
+ expect(result?.paymentTokenAddress).toBe(BASE_WETH);
179
+ expect(result?.paymentAmount).toBe(123n);
180
+ expect(result?.calls).toHaveLength(3);
181
+ expect(result?.calls[0]?.data).toMatch(/^0x2e1a7d4d/);
182
+ expect(result?.calls[1]).toMatchObject({
183
+ to: SEAPORT_ADDRESS,
184
+ value: 123n,
185
+ });
186
+ expect(result?.calls[2]?.to).toBe(COLLECTION_ADDRESS);
187
+
188
+ expect(() =>
189
+ encodeDestinationCalls({
190
+ calls: result?.calls ?? [],
191
+ tokenAddress: result?.paymentTokenAddress ?? BASE_WETH,
192
+ sweepTarget: USER_WALLET,
193
+ }),
194
+ ).not.toThrow();
195
+ });
196
+
197
+ it('falls back to order price as native value when the buy step value is empty', () => {
198
+ const result = build({
199
+ buyStep: { value: 0n, price: 0n },
200
+ marketOrder: {
201
+ priceCurrencyAddress: zeroAddress,
202
+ priceAmount: 123n,
203
+ },
204
+ });
205
+
206
+ expect(result?.paymentAmount).toBe(123n);
207
+ expect(result?.calls[1]).toMatchObject({
208
+ to: BUY_TARGET,
209
+ data: '0xabcdef',
210
+ value: 123n,
211
+ });
212
+ });
213
+ });