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

decentraland / marketplace / 6970244998

23 Nov 2023 01:18PM UTC coverage: 43.312% (+2.4%) from 40.865%
6970244998

Pull #2042

github

juanmahidalgo
feat: add missing select token and chain events
Pull Request #2042: feat: use squid to calculate route between two chains and tokens

2626 of 7307 branches covered (0.0%)

Branch coverage included in aggregate %.

277 of 382 new or added lines in 20 files covered. (72.51%)

1 existing line in 1 file now uncovered.

4634 of 9455 relevant lines covered (49.01%)

23.67 hits per line

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

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

82
export const NFT_SERVER_URL = config.get('NFT_SERVER_URL')!
27✔
83
export const CANCEL_FETCH_ITEMS = 'CANCEL_FETCH_ITEMS'
27✔
84

85
export function* itemSaga(getIdentity: () => AuthIdentity | undefined) {
86
  const API_OPTS = {
244✔
87
    retries: retryParams.attempts,
88
    retryDelay: retryParams.delay,
89
    identity: getIdentity
90
  }
91
  const itemAPI = new ItemAPI(NFT_SERVER_URL, API_OPTS)
244✔
92
  const marketplaceServerCatalogAPI = new CatalogAPI(
244✔
93
    MARKETPLACE_SERVER_URL,
94
    API_OPTS
95
  )
96
  const catalogAPI = new CatalogAPI(NFT_SERVER_URL, API_OPTS)
244✔
97

98
  yield fork(() => takeLatestByPath(FETCH_ITEMS_REQUEST, locations.browse()))
244✔
99
  yield takeEvery(
244✔
100
    FETCH_COLLECTION_ITEMS_REQUEST,
101
    handleFetchCollectionItemsRequest
102
  )
103
  yield takeEvery(FETCH_TRENDING_ITEMS_REQUEST, handleFetchTrendingItemsRequest)
244✔
104
  yield takeEvery(BUY_ITEM_REQUEST, handleBuyItem)
244✔
105
  yield takeEvery(BUY_ITEM_CROSS_CHAIN_REQUEST, handleBuyItemCrossChain)
244✔
106
  yield takeEvery(BUY_ITEM_WITH_CARD_REQUEST, handleBuyItemWithCardRequest)
244✔
107
  yield takeEvery(SET_PURCHASE, handleSetItemPurchaseWithCard)
244✔
108
  yield takeEvery(FETCH_ITEM_REQUEST, handleFetchItemRequest)
244✔
109

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

114
    while (true) {
244✔
115
      const action: FetchItemsRequestAction = yield take(actionType)
250✔
116
      const {
117
        pathname: currentPathname
118
      }: ReturnType<typeof getLocation> = yield select(getLocation)
6✔
119

120
      // if we have a task running in the browse path, we cancel the previous one
121
      if (matchPath(currentPathname, { path }) && task && task.isRunning()) {
6✔
122
        yield put({ type: CANCEL_FETCH_ITEMS }) // to unblock the saga waiting for the identity success
2✔
123
        yield cancel(task)
2✔
124
      }
125
      task = yield fork(handleFetchItemsRequest, action)
6✔
126
    }
127
  }
128

129
  function* handleFetchTrendingItemsRequest(
130
    action: FetchTrendingItemsRequestAction
131
  ) {
132
    const { size } = action.payload
3✔
133

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

136
    try {
3✔
137
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
3✔
138
      const { data }: { data: Item[] } = yield call(
3✔
139
        [itemAPI, 'getTrendings'],
140
        size
141
      )
142

143
      if (!data.length) {
2✔
144
        yield put(fetchTrendingItemsSuccess([]))
1✔
145
        return
1✔
146
      }
147

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

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

190
  function* handleFetchItemsRequest(
191
    action: FetchItemsRequestAction
192
  ): SagaIterator {
193
    const { filters, view } = action.payload
6✔
194

195
    try {
6✔
196
      // 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
197
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
6✔
198
      yield call(waitForFeatureFlagsToBeLoaded)
6✔
199
      const isMarketplaceServerEnabled: boolean = yield select(
6✔
200
        getIsMarketplaceServerEnabled
201
      )
202
      const catalogViewAPI = isMarketplaceServerEnabled
6✔
203
        ? marketplaceServerCatalogAPI
204
        : catalogAPI
205
      const api = isCatalogView(view) ? catalogViewAPI : itemAPI
6!
206
      const { data, total }: { data: Item[]; total: number } = yield call(
6✔
207
        [api, 'get'],
208
        filters
209
      )
210
      yield put(fetchItemsSuccess(data, total, action.payload, Date.now()))
3✔
211
    } catch (error) {
212
      yield put(
1✔
213
        fetchItemsFailure(
214
          isErrorWithMessage(error) ? error.message : t('global.unknown_error'),
1!
215
          action.payload
216
        )
217
      )
218
    } finally {
219
      if (yield cancelled()) {
6✔
220
        // if cancelled, we dispatch a failure action so it cleans the loading state
221
        yield put(
2✔
222
          fetchItemsFailure(FETCH_ITEMS_CANCELLED_ERROR_MESSAGE, action.payload)
223
        )
224
      }
225
    }
226
  }
227

228
  function* handleFetchItemRequest(action: FetchItemRequestAction) {
229
    const { contractAddress, tokenId } = action.payload
5✔
230

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

233
    try {
5✔
234
      yield call(waitForWalletConnectionAndIdentityIfConnecting)
5✔
235
      const item: Item = yield call(
5✔
236
        [itemAPI, 'getOne'],
237
        contractAddress,
238
        tokenId
239
      )
240
      yield put(fetchItemSuccess(item))
3✔
241
      yield put(fetchSmartWearableRequiredPermissionsRequest(item))
3✔
242
    } catch (error) {
243
      yield put(
1✔
244
        fetchItemFailure(
245
          contractAddress,
246
          tokenId,
247
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
248
        )
249
      )
250
    }
251
  }
252

253
  function* handleBuyItem(action: BuyItemRequestAction) {
254
    try {
3✔
255
      const { item } = action.payload
3✔
256

257
      const wallet: ReturnType<typeof getWallet> = yield select(getWallet)
3✔
258

259
      if (!wallet) {
3✔
260
        throw new Error('A defined wallet is required to buy an item')
1✔
261
      }
262

263
      const contract = getContract(ContractName.CollectionStore, item.chainId)
2✔
264

265
      const txHash: string = yield call(
2✔
266
        sendTransaction,
267
        contract,
268
        collectionStore =>
269
          collectionStore.buy([
×
270
            [
271
              item.contractAddress,
272
              [item.itemId],
273
              [item.price],
274
              [wallet.address]
275
            ]
276
          ])
277
      )
278

279
      yield put(buyItemSuccess(item.chainId, txHash, item))
1✔
280
    } catch (error) {
281
      yield put(
2✔
282
        buyItemFailure(
283
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
2!
284
        )
285
      )
286
    }
287
  }
288

289
  function* handleBuyItemCrossChain(action: BuyItemCrossChainRequestAction) {
NEW
290
    try {
×
NEW
291
      const { item, route } = action.payload
×
292

NEW
293
      const wallet: ReturnType<typeof getWallet> = yield select(getWallet)
×
294

NEW
295
      const provider: Provider | null = yield call(getConnectedProvider)
×
296

NEW
297
      if (!wallet) {
×
NEW
298
        throw new Error('A defined wallet is required to buy an item')
×
299
      }
300

NEW
301
      if (provider) {
×
NEW
302
        const txRespose: ethers.providers.TransactionResponse = yield call(
×
303
          [crossChainProvider, 'executeRoute'],
304
          route,
305
          provider
306
        )
307

NEW
308
        const tx: ethers.providers.TransactionReceipt = yield call(
×
309
          txRespose.wait
310
        )
NEW
311
        yield put(
×
312
          buyItemCrossChainSuccess(route, item.chainId, tx.transactionHash, item)
313
        )
314
      }
315
    } catch (error) {
NEW
316
      console.log('error: ', error)
×
NEW
317
      yield put(
×
318
        buyItemCrossChainFailure(
319
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
×
320
        )
321
      )
322
    }
323
  }
324

325
  function* handleBuyItemWithCardRequest(action: BuyItemWithCardRequestAction) {
326
    try {
4✔
327
      const { item } = action.payload
4✔
328
      yield call(buyAssetWithCard, item)
4✔
329
    } catch (error) {
330
      yield put(
1✔
331
        buyItemWithCardFailure(
332
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
333
        )
334
      )
335
    }
336
  }
337

338
  function* handleSetItemPurchaseWithCard(action: SetPurchaseAction) {
339
    try {
7✔
340
      const { purchase } = action.payload
7✔
341
      const { status, txHash } = purchase
7✔
342

343
      if (
7✔
344
        isNFTPurchase(purchase) &&
22✔
345
        purchase.nft.itemId &&
346
        status === PurchaseStatus.COMPLETE &&
347
        txHash
348
      ) {
349
        const {
350
          nft: { contractAddress, itemId }
351
        } = purchase
3✔
352

353
        const items: ReturnType<typeof getItems> = yield select(getItems)
3✔
354
        let item: ReturnType<typeof getItem> = yield call(
3✔
355
          getItem,
356
          contractAddress,
357
          itemId,
358
          items
359
        )
360

361
        if (!item) {
3✔
362
          yield put(fetchItemRequest(contractAddress, itemId))
2✔
363

364
          const {
365
            success,
366
            failure
367
          }: {
368
            success: FetchItemSuccessAction
369
            failure: FetchItemFailureAction
370
          } = yield race({
2✔
371
            success: take(FETCH_ITEM_SUCCESS),
372
            failure: take(FETCH_ITEM_FAILURE)
373
          })
374

375
          if (failure) throw new Error(failure.payload.error)
1!
376

377
          item = success.payload.item
×
378
        }
379

380
        yield put(buyItemWithCardSuccess(item.chainId, txHash, item, purchase))
1✔
381
      }
382
    } catch (error) {
383
      yield put(
1✔
384
        buyItemWithCardFailure(
385
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
386
        )
387
      )
388
    }
389
  }
390
}
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