• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

decentraland / marketplace / 8143828014

04 Mar 2024 04:43PM UTC coverage: 66.207% (-0.02%) from 66.231%
8143828014

Pull #2165

github

LautaroPetaccio
fix: Removed unused variables
Pull Request #2165: fix: Remove emotes v2 FTU

2516 of 4915 branches covered (51.19%)

Branch coverage included in aggregate %.

7615 of 10387 relevant lines covered (73.31%)

70.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.25
/webapp/src/modules/item/sagas.ts
1
import { matchPath } from 'react-router-dom'
29✔
2
import { getLocation, push } from 'connected-react-router'
29✔
3
import { SagaIterator } from 'redux-saga'
4
import { put, takeEvery } from '@redux-saga/core/effects'
29✔
5
import {
6
  call,
7
  cancel,
8
  cancelled,
9
  delay,
10
  fork,
11
  race,
12
  select,
13
  take
14
} from 'redux-saga/effects'
29✔
15
import { ethers } from 'ethers'
16
import { ChainId, Item } from '@dcl/schemas'
17
import { getConnectedProvider } from 'decentraland-dapps/dist/lib/eth'
29✔
18
import { ContractName, getContract } from 'decentraland-transactions'
29✔
19
import { Provider } from 'decentraland-connect'
20
import { AuthIdentity } from 'decentraland-crypto-fetch'
21
import { sendTransaction } from 'decentraland-dapps/dist/modules/wallet/utils'
29✔
22
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
29✔
23
import {
24
  SetPurchaseAction,
25
  SET_PURCHASE
26
} from 'decentraland-dapps/dist/modules/gateway/actions'
29✔
27
import { isNFTPurchase } from 'decentraland-dapps/dist/modules/gateway/utils'
29✔
28
import { PurchaseStatus } from 'decentraland-dapps/dist/modules/gateway/types'
29✔
29
import { isErrorWithMessage } from '../../lib/error'
29✔
30
import type { StatusResponse } from 'decentraland-transactions/crossChain'
31
import { config } from '../../config'
29✔
32
import { ItemAPI } from '../vendor/decentraland/item/api'
29✔
33
import { getWallet } from '../wallet/selectors'
29✔
34
import { buyAssetWithCard } from '../asset/utils'
29✔
35
import { isCatalogView } from '../routing/utils'
29✔
36
import { waitForWalletConnectionAndIdentityIfConnecting } from '../wallet/utils'
29✔
37
import { retryParams } from '../vendor/decentraland/utils'
29✔
38
import { CatalogAPI } from '../vendor/decentraland/catalog/api'
29✔
39
import { locations } from '../routing/locations'
29✔
40
import { fetchSmartWearableRequiredPermissionsRequest } from '../asset/actions'
29✔
41
import { MARKETPLACE_SERVER_URL } from '../vendor/decentraland'
29✔
42
import { getIsMarketplaceServerEnabled } from '../features/selectors'
29✔
43
import { waitForFeatureFlagsToBeLoaded } from '../features/utils'
29✔
44
import {
45
  buyItemFailure,
46
  BuyItemRequestAction,
47
  buyItemSuccess,
48
  BUY_ITEM_REQUEST,
49
  fetchItemsFailure,
50
  FetchItemsRequestAction,
51
  fetchItemsSuccess,
52
  FETCH_ITEMS_REQUEST,
53
  fetchItemFailure,
54
  FetchItemRequestAction,
55
  fetchItemSuccess,
56
  FETCH_ITEM_REQUEST,
57
  FETCH_TRENDING_ITEMS_REQUEST,
58
  FetchTrendingItemsRequestAction,
59
  fetchTrendingItemsSuccess,
60
  fetchTrendingItemsFailure,
61
  BuyItemWithCardRequestAction,
62
  BUY_ITEM_WITH_CARD_REQUEST,
63
  buyItemWithCardSuccess,
64
  buyItemWithCardFailure,
65
  FetchItemFailureAction,
66
  FetchItemSuccessAction,
67
  FETCH_ITEM_FAILURE,
68
  FETCH_ITEM_SUCCESS,
69
  fetchItemRequest,
70
  FetchCollectionItemsRequestAction,
71
  fetchCollectionItemsSuccess,
72
  fetchCollectionItemsFailure,
73
  FETCH_COLLECTION_ITEMS_REQUEST,
74
  FETCH_ITEMS_CANCELLED_ERROR_MESSAGE,
75
  BuyItemCrossChainRequestAction,
76
  BUY_ITEM_CROSS_CHAIN_REQUEST,
77
  buyItemCrossChainSuccess,
78
  buyItemCrossChainFailure,
79
  BUY_ITEM_CROSS_CHAIN_SUCCESS,
80
  BuyItemCrossChainSuccessAction,
81
  trackCrossChainTx
82
} from './actions'
29✔
83
import { getData as getItems } from './selectors'
29✔
84
import { AssetType } from '../asset/types'
29✔
85
import { getItem } from './utils'
29✔
86

87
export const NFT_SERVER_URL = config.get('NFT_SERVER_URL')!
29✔
88
export const CANCEL_FETCH_ITEMS = 'CANCEL_FETCH_ITEMS'
29✔
89

90
export function* itemSaga(getIdentity: () => AuthIdentity | undefined) {
264✔
91
  const API_OPTS = {
264✔
92
    retries: retryParams.attempts,
93
    retryDelay: retryParams.delay,
94
    identity: getIdentity
95
  }
96
  const itemAPI = new ItemAPI(NFT_SERVER_URL, API_OPTS)
264✔
97
  const marketplaceServerCatalogAPI = new CatalogAPI(
264✔
98
    MARKETPLACE_SERVER_URL,
99
    API_OPTS
100
  )
101
  const catalogAPI = new CatalogAPI(NFT_SERVER_URL, API_OPTS)
264✔
102

103
  yield fork(() => takeLatestByPath(FETCH_ITEMS_REQUEST, locations.browse()))
264✔
104
  yield takeEvery(
264✔
105
    FETCH_COLLECTION_ITEMS_REQUEST,
106
    handleFetchCollectionItemsRequest
107
  )
108
  yield takeEvery(FETCH_TRENDING_ITEMS_REQUEST, handleFetchTrendingItemsRequest)
264✔
109
  yield takeEvery(BUY_ITEM_REQUEST, handleBuyItem)
264✔
110
  yield takeEvery(BUY_ITEM_CROSS_CHAIN_REQUEST, handleBuyItemCrossChain)
264✔
111
  yield takeEvery(BUY_ITEM_CROSS_CHAIN_SUCCESS, handleBuyItemCrossChainSuccess)
264✔
112
  yield takeEvery(BUY_ITEM_WITH_CARD_REQUEST, handleBuyItemWithCardRequest)
264✔
113
  yield takeEvery(SET_PURCHASE, handleSetItemPurchaseWithCard)
264✔
114
  yield takeEvery(FETCH_ITEM_REQUEST, handleFetchItemRequest)
264✔
115

116
  // to avoid race conditions, just one fetch items request is handled at once in the browse page
117
  function* takeLatestByPath(actionType: string, path: string): SagaIterator {
118
    let task
119

120
    while (true) {
264✔
121
      const action: FetchItemsRequestAction = yield take(actionType)
270✔
122
      const {
123
        pathname: currentPathname
124
      }: ReturnType<typeof getLocation> = yield select(getLocation)
6✔
125

126
      // if we have a task running in the browse path, we cancel the previous one
127
      if (matchPath(currentPathname, { path }) && task && task.isRunning()) {
6✔
128
        yield put({ type: CANCEL_FETCH_ITEMS }) // to unblock the saga waiting for the identity success
129
        yield cancel(task)
2✔
130
      }
131
      task = yield fork(handleFetchItemsRequest, action)
6✔
132
    }
133
  }
134

135
  function* handleFetchTrendingItemsRequest(
136
    action: FetchTrendingItemsRequestAction
137
  ) {
138
    const { size } = action.payload
3✔
139

140
    // If the wallet is getting connected, wait until it finishes to fetch the items so it can fetch them with authentication
141

142
    try {
3✔
143
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
3✔
144
      const { data }: { data: Item[] } = yield call(
3✔
145
        [itemAPI, 'getTrendings'],
146
        size
147
      )
148

149
      if (!data.length) {
2✔
150
        yield put(fetchTrendingItemsSuccess([]))
1✔
151
        return
1✔
152
      }
153

154
      const ids = data.map(item => item.id)
1✔
155
      const isMarketplaceServerEnabled: boolean = yield select(
1✔
156
        getIsMarketplaceServerEnabled
157
      )
158
      const api = isMarketplaceServerEnabled
1!
159
        ? marketplaceServerCatalogAPI
160
        : catalogAPI
161
      const { data: itemData }: { data: Item[]; total: number } = yield call(
1✔
162
        [api, 'get'],
163
        {
164
          ids
165
        }
166
      )
167
      yield put(fetchTrendingItemsSuccess(itemData))
1✔
168
    } catch (error) {
169
      yield put(
1✔
170
        fetchTrendingItemsFailure(
171
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
172
        )
173
      )
174
    }
175
  }
176

177
  function* handleFetchCollectionItemsRequest(
178
    action: FetchCollectionItemsRequestAction
179
  ) {
180
    const { contractAddresses, first } = action.payload
2✔
181
    try {
2✔
182
      const { data }: { data: Item[]; total: number } = yield call(
2✔
183
        [itemAPI, 'get'],
184
        { first, contractAddresses }
185
      )
186
      yield put(fetchCollectionItemsSuccess(data))
1✔
187
    } catch (error) {
188
      yield put(
1✔
189
        fetchCollectionItemsFailure(
190
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
191
        )
192
      )
193
    }
194
  }
195

196
  function* handleFetchItemsRequest(
197
    action: FetchItemsRequestAction
198
  ): SagaIterator {
199
    const { filters, view } = action.payload
6✔
200

201
    try {
6✔
202
      // If the wallet is getting connected, wait until it finishes to fetch the wallet and generate the identity so it can fetch them with authentication
203
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
6✔
204
      yield call(waitForFeatureFlagsToBeLoaded)
6✔
205
      const isMarketplaceServerEnabled: boolean = yield select(
6✔
206
        getIsMarketplaceServerEnabled
207
      )
208
      const catalogViewAPI = isMarketplaceServerEnabled
6✔
209
        ? marketplaceServerCatalogAPI
210
        : catalogAPI
211
      const api = isCatalogView(view) ? catalogViewAPI : itemAPI
6!
212
      const { data, total }: { data: Item[]; total: number } = yield call(
6✔
213
        [api, 'get'],
214
        filters
215
      )
216
      yield put(fetchItemsSuccess(data, total, action.payload, Date.now()))
3✔
217
    } catch (error) {
218
      yield put(
1✔
219
        fetchItemsFailure(
220
          isErrorWithMessage(error) ? error.message : t('global.unknown_error'),
1!
221
          action.payload
222
        )
223
      )
224
    } finally {
225
      if (yield cancelled()) {
6✔
226
        // if cancelled, we dispatch a failure action so it cleans the loading state
227
        yield put(
2✔
228
          fetchItemsFailure(FETCH_ITEMS_CANCELLED_ERROR_MESSAGE, action.payload)
229
        )
230
      }
231
    }
232
  }
233

234
  function* handleFetchItemRequest(action: FetchItemRequestAction) {
235
    const { contractAddress, tokenId } = action.payload
3✔
236

237
    // If the wallet is getting connected, wait until it finishes to fetch the items so it can fetch them with authentication
238

239
    try {
3✔
240
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
3✔
241
      const item: Item = yield call(
3✔
242
        [itemAPI, 'getOne'],
243
        contractAddress,
244
        tokenId
245
      )
246
      yield put(fetchItemSuccess(item))
2✔
247
      if (item.data?.wearable?.isSmart && item.urn) {
2✔
248
        yield put(fetchSmartWearableRequiredPermissionsRequest(item))
1✔
249
      }
250
    } catch (error) {
251
      yield put(
1✔
252
        fetchItemFailure(
253
          contractAddress,
254
          tokenId,
255
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
256
        )
257
      )
258
    }
259
  }
260

261
  function* handleBuyItem(action: BuyItemRequestAction) {
262
    try {
3✔
263
      const { item } = action.payload
3✔
264

265
      const wallet: ReturnType<typeof getWallet> = yield select(getWallet)
3✔
266

267
      if (!wallet) {
3✔
268
        throw new Error('A defined wallet is required to buy an item')
1✔
269
      }
270

271
      const contract = getContract(ContractName.CollectionStore, item.chainId)
2✔
272

273
      const txHash: string = yield call(
2✔
274
        sendTransaction,
275
        contract,
276
        collectionStore =>
277
          collectionStore.buy([
×
278
            [
279
              item.contractAddress,
280
              [item.itemId],
281
              [item.price],
282
              [wallet.address]
283
            ]
284
          ])
285
      )
286

287
      yield put(buyItemSuccess(wallet.chainId, txHash, item))
1✔
288
    } catch (error) {
289
      yield put(
2✔
290
        buyItemFailure(
291
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
2!
292
        )
293
      )
294
    }
295
  }
296

297
  function* handleBuyItemCrossChainSuccess(
298
    action: BuyItemCrossChainSuccessAction
299
  ) {
300
    const { route, item, order, txHash } = action.payload
1✔
301
    // if it's an actual cross-chain interaction, we need to get the tx hash in the destination chain
302
    if (
1✔
303
      route.requestId &&
2✔
304
      route.route.params.fromChain !== route.route.params.toChain
305
    ) {
306
      let status: StatusResponse | undefined
307
      const crossChainModule = import('decentraland-transactions/crossChain')
2✔
308
      const {
309
        AxelarProvider
310
      }: Awaited<typeof crossChainModule> = yield crossChainModule
1✔
311
      const crossChainProvider = new AxelarProvider(config.get('SQUID_API_URL'))
1✔
312
      const destinationChain = Number(route.route.params.toChain) as ChainId
1✔
313
      while (!status || !status?.toChain?.transactionId) {
1✔
314
        // wrapping in try-catch since it throws an error if the tx is not found (the first seconds after triggering it)
315
        try {
1✔
316
          status = yield call(
1✔
317
            [crossChainProvider, 'getStatus'],
318
            route.requestId,
319
            txHash
320
          )
321
        } catch (error) {
322
          console.error('error: ', error)
×
323
        }
324
        yield delay(1000)
1✔
325
      }
326
      if (status?.toChain?.transactionId) {
1✔
327
        yield put(
1✔
328
          trackCrossChainTx(destinationChain, status?.toChain?.transactionId)
329
        )
330
        yield put(
1✔
331
          push(
332
            locations.success({
333
              txHash,
334
              destinationTxHash: status?.toChain?.transactionId,
335
              tokenId: item.itemId,
336
              assetType: order ? AssetType.NFT : AssetType.ITEM,
1!
337
              contractAddress: order
1!
338
                ? order.contractAddress
339
                : item.contractAddress,
340
              isCrossChain: ('route' in action.payload).toString()
341
            })
342
          )
343
        )
344
      }
345
    }
346
  }
347

348
  function* handleBuyItemCrossChain(action: BuyItemCrossChainRequestAction) {
349
    const { item, route, order } = action.payload
×
350
    try {
×
351
      const wallet: ReturnType<typeof getWallet> = yield select(getWallet)
×
352

353
      const provider: Provider | null = yield call(getConnectedProvider)
×
354

355
      if (!wallet) {
×
356
        throw new Error('A defined wallet is required to buy an item')
×
357
      }
358

359
      if (provider) {
×
360
        const crossChainModule = import('decentraland-transactions/crossChain')
×
361
        const {
362
          AxelarProvider
363
        }: Awaited<typeof crossChainModule> = yield crossChainModule
×
364

365
        const crossChainProvider = new AxelarProvider(
×
366
          config.get('SQUID_API_URL')
367
        )
368
        const txResponse: ethers.providers.TransactionReceipt = yield call(
×
369
          [crossChainProvider, 'executeRoute'],
370
          route,
371
          provider
372
        )
373

374
        yield put(
×
375
          buyItemCrossChainSuccess(
376
            route,
377
            Number(route.route.params.fromChain),
378
            txResponse.transactionHash,
379
            item,
380
            order
381
          )
382
        )
383
      }
384
    } catch (error) {
385
      yield put(
×
386
        buyItemCrossChainFailure(
387
          route,
388
          item,
389
          order?.price || item.price,
×
390
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
×
391
        )
392
      )
393
    }
394
  }
395

396
  function* handleBuyItemWithCardRequest(action: BuyItemWithCardRequestAction) {
397
    try {
4✔
398
      const { item } = action.payload
4✔
399
      yield call(buyAssetWithCard, item)
4✔
400
    } catch (error) {
401
      yield put(
1✔
402
        buyItemWithCardFailure(
403
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
404
        )
405
      )
406
    }
407
  }
408

409
  function* handleSetItemPurchaseWithCard(action: SetPurchaseAction) {
410
    try {
7✔
411
      const { purchase } = action.payload
7✔
412
      const { status, txHash } = purchase
7✔
413

414
      if (
7✔
415
        isNFTPurchase(purchase) &&
22✔
416
        purchase.nft.itemId &&
417
        status === PurchaseStatus.COMPLETE &&
418
        txHash
419
      ) {
420
        const {
421
          nft: { contractAddress, itemId }
422
        } = purchase
3✔
423

424
        const items: ReturnType<typeof getItems> = yield select(getItems)
3✔
425
        let item: ReturnType<typeof getItem> = yield call(
3✔
426
          getItem,
427
          contractAddress,
428
          itemId,
429
          items
430
        )
431

432
        if (!item) {
3✔
433
          yield put(fetchItemRequest(contractAddress, itemId))
2✔
434

435
          const {
436
            success,
437
            failure
438
          }: {
439
            success: FetchItemSuccessAction
440
            failure: FetchItemFailureAction
441
          } = yield race({
2✔
442
            success: take(FETCH_ITEM_SUCCESS),
443
            failure: take(FETCH_ITEM_FAILURE)
444
          })
445

446
          if (failure) throw new Error(failure.payload.error)
1✔
447

448
          item = success.payload.item
×
449
        }
450

451
        yield put(buyItemWithCardSuccess(item.chainId, txHash, item, purchase))
1✔
452
      }
453
    } catch (error) {
454
      yield put(
1✔
455
        buyItemWithCardFailure(
456
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
457
        )
458
      )
459
    }
460
  }
461
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc