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

decentraland / marketplace / 8194016372

07 Mar 2024 07:54PM UTC coverage: 66.152% (-0.09%) from 66.242%
8194016372

Pull #2167

github

LautaroPetaccio
fix: Add toasts
Pull Request #2167: feat: Use new cross chain transaction payload action

2507 of 4904 branches covered (51.12%)

Branch coverage included in aggregate %.

8 of 13 new or added lines in 6 files covered. (61.54%)

6 existing lines in 5 files now uncovered.

7601 of 10376 relevant lines covered (73.26%)

70.55 hits per line

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

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

81
export const NFT_SERVER_URL = config.get('NFT_SERVER_URL')!
29✔
82
export const CANCEL_FETCH_ITEMS = 'CANCEL_FETCH_ITEMS'
29✔
83

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

290
  function* handleBuyItemCrossChain(action: BuyItemCrossChainRequestAction) {
UNCOV
291
    const { item, route, order } = action.payload
×
292
    try {
×
NEW
293
      console.log('Buying item cross chain')
×
UNCOV
294
      const wallet: ReturnType<typeof getWallet> = yield select(getWallet)
×
295

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

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

302
      if (provider) {
×
303
        const crossChainModule = import('decentraland-transactions/crossChain')
×
304
        const {
305
          AxelarProvider
306
        }: Awaited<typeof crossChainModule> = yield crossChainModule
×
307
        const crossChainProvider = new AxelarProvider(
×
308
          config.get('SQUID_API_URL')
309
        )
310
        const txResponse: ethers.providers.TransactionReceipt = yield call(
×
311
          [crossChainProvider, 'executeRoute'],
312
          route,
313
          provider
314
        )
315

316
        yield put(
×
317
          buyItemCrossChainSuccess(
318
            route,
319
            Number(route.route.params.fromChain),
320
            txResponse.transactionHash,
321
            item,
322
            order
323
          )
324
        )
325
      }
326
    } catch (error) {
327
      yield put(
×
328
        buyItemCrossChainFailure(
329
          route,
330
          item,
331
          order?.price || item.price,
×
332
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
×
333
        )
334
      )
335
    }
336
  }
337

338
  function* handleBuyItemWithCardRequest(action: BuyItemWithCardRequestAction) {
339
    try {
4✔
340
      const { item } = action.payload
4✔
341
      yield call(buyAssetWithCard, item)
4✔
342
    } catch (error) {
343
      yield put(
1✔
344
        buyItemWithCardFailure(
345
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
346
        )
347
      )
348
    }
349
  }
350

351
  function* handleSetItemPurchaseWithCard(action: SetPurchaseAction) {
352
    try {
7✔
353
      const { purchase } = action.payload
7✔
354
      const { status, txHash } = purchase
7✔
355

356
      if (
7✔
357
        isNFTPurchase(purchase) &&
22✔
358
        purchase.nft.itemId &&
359
        status === PurchaseStatus.COMPLETE &&
360
        txHash
361
      ) {
362
        const {
363
          nft: { contractAddress, itemId }
364
        } = purchase
3✔
365

366
        const items: ReturnType<typeof getItems> = yield select(getItems)
3✔
367
        let item: ReturnType<typeof getItem> = yield call(
3✔
368
          getItem,
369
          contractAddress,
370
          itemId,
371
          items
372
        )
373

374
        if (!item) {
3✔
375
          yield put(fetchItemRequest(contractAddress, itemId))
2✔
376

377
          const {
378
            success,
379
            failure
380
          }: {
381
            success: FetchItemSuccessAction
382
            failure: FetchItemFailureAction
383
          } = yield race({
2✔
384
            success: take(FETCH_ITEM_SUCCESS),
385
            failure: take(FETCH_ITEM_FAILURE)
386
          })
387

388
          if (failure) throw new Error(failure.payload.error)
1✔
389

390
          item = success.payload.item
×
391
        }
392

393
        yield put(buyItemWithCardSuccess(item.chainId, txHash, item, purchase))
1✔
394
      }
395
    } catch (error) {
396
      yield put(
1✔
397
        buyItemWithCardFailure(
398
          isErrorWithMessage(error) ? error.message : t('global.unknown_error')
1!
399
        )
400
      )
401
    }
402
  }
403
}
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