@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.
- package/CHANGELOG.md +10 -0
- package/dist/BellIcon.js +1 -1
- package/dist/Card.js +1 -1
- package/dist/ShopCard.d.ts +4 -4
- package/dist/builder-api.js +1 -1
- package/dist/collectible.js +1 -1
- package/dist/collection.js +1 -1
- package/dist/create-config.d.ts +597 -201
- package/dist/create-config.js +1 -1
- package/dist/currency.js +1 -1
- package/dist/dist.js +167 -148
- package/dist/dist.js.map +1 -1
- package/dist/expirationDateSelect.js +1 -1
- package/dist/filter-state.d.ts +1 -1
- package/dist/filters.d.ts +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index10.d.ts +3 -3
- package/dist/index11.d.ts +17 -17
- package/dist/index12.d.ts +26 -26
- package/dist/index14.d.ts +3 -3
- package/dist/index15.d.ts +3 -3
- package/dist/index16.d.ts +2 -2
- package/dist/index17.d.ts +86 -86
- package/dist/index18.d.ts +40 -40
- package/dist/index19.d.ts +5 -5
- package/dist/index2.d.ts +4 -1
- package/dist/index21.d.ts +15 -15
- package/dist/index22.d.ts +8 -65
- package/dist/index23.d.ts +21 -13
- package/dist/index26.d.ts +4 -4
- package/dist/index27.d.ts +4 -4
- package/dist/index28.d.ts +10 -10
- package/dist/index3.d.ts +2 -2194
- package/dist/index31.d.ts +5 -5
- package/dist/index33.d.ts +3 -3
- package/dist/index34.d.ts +4 -4
- package/dist/index35.d.ts +1 -1
- package/dist/index36.d.ts +7 -7
- package/dist/index37.d.ts +6 -6
- package/dist/index38.d.ts +8 -8
- package/dist/index39.d.ts +1 -1
- package/dist/index4.d.ts +995 -995
- package/dist/index40.d.ts +2 -2
- package/dist/index8.d.ts +2 -2
- package/dist/index9.d.ts +2811 -3
- package/dist/inventory.d.ts +4 -4
- package/dist/inventory.js +2 -2
- package/dist/marketplace2.js +2 -2
- package/dist/metadata.d.ts +73 -73
- package/dist/primary-sale-checkout-options.d.ts +4 -4
- package/dist/quantityInput.js +1 -1
- package/dist/ranges.d.ts +17 -17
- package/dist/react/_internal/index.d.ts +1 -1
- package/dist/react/_internal/index.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/queries/collectible/index.d.ts +1 -1
- package/dist/react/queries/index.d.ts +1 -1
- package/dist/react/ssr/index.d.ts +1 -1
- package/dist/react/ssr/index.js +2 -2
- package/dist/react/ui/components/marketplace-collectible-card/index.d.ts +1 -1
- package/dist/react/ui/components/marketplace-logos/index.d.ts +21 -21
- package/dist/react/ui/modals/CreateListingModal/internal/hooks/index.d.ts +1 -1
- package/dist/react/ui/modals/MakeOfferModal/internal/hooks/index.d.ts +1 -1
- package/dist/react/ui/modals/_internal/components/alertMessage/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/baseModal/index.d.ts +6 -6
- package/dist/react/ui/modals/_internal/components/calendar/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/calendarDropdown/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/currencyImage/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/currencyOptionsSelect/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/floorPriceText/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/priceInput/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/quantityInput/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/selectWaasFeeOptions/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/switchChainErrorModal/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/timeAgo/index.d.ts +2 -2
- package/dist/react/ui/modals/_internal/components/tokenPreview/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/transaction-footer/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/transactionDetails/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/transactionPreview/index.d.ts +3 -3
- package/dist/react/ui/modals/_internal/components/transactionStatusModal/index.d.ts +3 -3
- package/dist/react.js +2192 -1903
- package/dist/react.js.map +1 -1
- package/dist/token-balances.d.ts +28 -28
- package/dist/transaction-footer.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/url-state.js +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils.js +20 -4
- package/dist/utils.js.map +1 -1
- package/package.json +7 -5
- package/src/react/hooks/config/useMarketplaceConfig.test.tsx +1 -0
- package/src/react/hooks/transactions/useCancelTransactionSteps.tsx +4 -1
- package/src/react/hooks/utils/useEnsureCorrectChain.ts +10 -5
- package/src/react/ui/modals/BuyModal/components/BuyModalContent.tsx +34 -31
- package/src/react/ui/modals/BuyModal/components/CryptoPaymentModal.tsx +74 -11
- package/src/react/ui/modals/BuyModal/components/Modal.tsx +62 -1
- package/src/react/ui/modals/BuyModal/hooks/useExecuteBundledTransactions.ts +13 -26
- package/src/react/ui/modals/BuyModal/internal/__tests__/buildTrailsMarketBuyActions.test.ts +213 -0
- package/src/react/ui/modals/BuyModal/internal/buildTrailsMarketBuyActions.ts +259 -0
- package/src/react/ui/modals/BuyModal/internal/buyModalContext.ts +79 -10
- package/src/react/ui/modals/BuyModal/internal/cryptoPaymentModalContext.tsx +44 -17
- package/src/react/ui/modals/MakeOfferModal/internal/context.ts +2 -1
- package/src/react/ui/modals/_internal/components/currencyOptionsSelect/index.tsx +2 -1
- package/src/react/ui/modals/_internal/components/priceInput/index.tsx +12 -11
- package/src/react/ui/modals/_internal/helpers/currency.test.ts +27 -0
- package/src/react/ui/modals/_internal/helpers/currency.ts +4 -2
- package/src/utils/__tests__/getMarketplaceDetails.test.ts +10 -0
- package/src/utils/__tests__/getWebRPCErrorMessage.test.ts +28 -0
- package/src/utils/__tests__/marketplaceNormalization.test.ts +38 -0
- package/src/utils/getConduitAddressForOrderbook.ts +2 -10
- package/src/utils/getMarketplaceDetails.ts +11 -4
- package/src/utils/getWebRPCErrorMessage.ts +21 -0
- 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 {
|
|
2
|
-
|
|
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 {
|
|
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
|
|
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 &&
|
|
71
|
-
? formatUnits(
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
});
|