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

decentraland / marketplace / 20164899792

12 Dec 2025 11:09AM UTC coverage: 66.467% (+0.3%) from 66.153%
20164899792

push

github

web-flow
feat: introduce use of credits for NAMEs (#2543)

* feat: introduce use of credits for NAMEs

* feat: bump dcl libs

* feat: update Payment Selector component

* test: update test missing prop

* feat: add tests and dev.json updated

* feat: add more tests and update old ones

* fix: build issue

* fix: test

* feat: add step to show coral scan link for the users (#2544)

* feat: add step to show coral scan link for the users

* fix: test

2825 of 5527 branches covered (51.11%)

Branch coverage included in aggregate %.

136 of 151 new or added lines in 10 files covered. (90.07%)

1 existing line in 1 file now uncovered.

8263 of 11155 relevant lines covered (74.07%)

75.82 hits per line

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

45.95
/webapp/src/components/Modals/BuyWithCryptoModal/hooks.ts
1
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5✔
2
import { BigNumber, ethers } from 'ethers'
5✔
3
import { ChainId, Item, Network, Order } from '@dcl/schemas'
5✔
4
import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id'
5✔
5
import { getNetworkProvider } from 'decentraland-dapps/dist/lib'
5✔
6
import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics'
5✔
7
import { TradeService } from 'decentraland-dapps/dist/modules/trades/TradeService'
5✔
8
import { Wallet } from 'decentraland-dapps/dist/modules/wallet'
9
import type { CrossChainProvider, Route, RouteResponse, Token } from 'decentraland-transactions/crossChain'
10
import { ContractName, getContract } from 'decentraland-transactions'
5✔
11
import { API_SIGNER } from '../../../lib/api'
5✔
12
import { NFT } from '../../../modules/nft/types'
13
import { MARKETPLACE_SERVER_URL } from '../../../modules/vendor/decentraland'
5✔
14
import * as events from '../../../utils/events'
5✔
15
import { getOnChainTrade } from '../../../utils/trades'
5✔
16
import { estimateBuyNftGas, estimateMintNftGas, estimateNameMintingGas, formatPrice, getShouldUseMetaTx, getTokenBalance } from './utils'
5✔
17

18
export const NATIVE_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
9✔
19
const ROUTE_FETCH_INTERVAL = 10000000 // 10 secs
5✔
20

21
export const useShouldUseCrossChainProvider = (selectedToken: Token, assetNetwork: Network) => {
8✔
22
  return useMemo(
242✔
23
    () =>
24
      !(
73✔
25
        (selectedToken.symbol === 'MANA' &&
282✔
26
          getNetwork(parseInt(selectedToken.chainId) as ChainId) === Network.MATIC &&
27
          assetNetwork === Network.MATIC) || // MANA selected and it's sending the tx from MATIC
28
        (selectedToken.symbol === 'MANA' &&
29
          getNetwork(parseInt(selectedToken.chainId) as ChainId) === Network.ETHEREUM &&
30
          assetNetwork === Network.ETHEREUM)
31
      ), // MANA selected and it's connected to ETH and buying a L1 NFT
32
    [assetNetwork, selectedToken]
33
  )
34
}
35

36
export type TokenBalance = {
37
  isFetchingBalance: boolean
38
  tokenBalance: BigNumber | undefined
39
}
40

41
// Retrieves the token balance for the selected token in the selected chain for the user's address
42
export const useTokenBalance = (selectedToken: Token, selectedChain: ChainId, address: string | undefined | null): TokenBalance => {
13✔
43
  const [isFetchingBalance, setIsFetchingBalance] = useState(false)
12✔
44
  const [selectedTokenBalance, setSelectedTokenBalance] = useState<BigNumber>()
12✔
45

46
  // fetch selected token balance & price
47
  useEffect(() => {
12✔
48
    let cancel = false
4✔
49
    void (async () => {
4✔
50
      try {
4✔
51
        setIsFetchingBalance(true)
4✔
52
        if (
4✔
53
          selectedToken.symbol !== 'MANA' && // mana balance is already available in the wallet
7✔
54
          address
55
        ) {
56
          const balance = await getTokenBalance(selectedToken, selectedChain, address)
2✔
57
          if (!cancel) {
2✔
58
            setSelectedTokenBalance(balance)
2✔
59
          }
60
        }
61
      } catch (error) {
62
        console.error('Error getting balance: ', error)
×
63
      } finally {
64
        if (!cancel) {
4✔
65
          setIsFetchingBalance(false)
4✔
66
        }
67
      }
68
    })()
69
    return () => {
4✔
70
      cancel = true
4✔
71
    }
72
  }, [selectedToken, selectedChain, address])
73

74
  return { isFetchingBalance, tokenBalance: selectedTokenBalance }
12✔
75
}
76

77
export type GasCostValues = {
78
  total: string
79
  token: Token | undefined
80
  totalUSDPrice: number | undefined
81
}
82

83
export type GasCost = {
84
  gasCost: GasCostValues | undefined
85
  isFetchingGasCost: boolean
86
}
87

88
const useGasCost = (
5✔
89
  assetNetwork: Network,
90
  nativeChainToken: Token | undefined,
91
  selectedChain: ChainId,
92
  shouldUseCrossChainProvider: boolean,
93
  wallet: Wallet | undefined | null,
94
  estimateTransactionGas: () => Promise<BigNumber | undefined>
95
): GasCost => {
96
  const [gasCost, setGasCost] = useState<GasCostValues>()
30✔
97
  const [isFetchingGasCost, setIsFetchingGasCost] = useState(false)
30✔
98

99
  useEffect(() => {
30✔
100
    const calculateGas = async () => {
15✔
101
      try {
6✔
102
        setIsFetchingGasCost(true)
6✔
103
        const networkProvider = await getNetworkProvider(selectedChain)
6✔
104
        const provider = new ethers.providers.Web3Provider(networkProvider)
6✔
105
        const gasPrice: BigNumber = await provider.getGasPrice()
6✔
106
        const estimation = await estimateTransactionGas()
6✔
107

108
        if (estimation) {
3✔
109
          const total = estimation.mul(gasPrice)
3✔
110
          const totalUSDPrice = nativeChainToken?.usdPrice ? nativeChainToken.usdPrice * +ethers.utils.formatEther(total) : undefined
3!
111

112
          setGasCost({
3✔
113
            token: nativeChainToken,
114
            total: ethers.utils.formatEther(total),
115
            totalUSDPrice
116
          })
117
        }
118
        setIsFetchingGasCost(false)
3✔
119
      } catch (error) {
120
        setIsFetchingGasCost(false)
3✔
121
      }
122
    }
123

124
    if (!shouldUseCrossChainProvider && wallet && nativeChainToken && getNetwork(wallet.chainId) === assetNetwork) {
15✔
125
      void calculateGas()
6✔
126
    } else {
127
      setGasCost(undefined)
9✔
128
    }
129
  }, [assetNetwork, estimateTransactionGas, nativeChainToken, selectedChain, shouldUseCrossChainProvider, wallet])
130

131
  return { gasCost, isFetchingGasCost }
30✔
132
}
133

134
export const useMintingNftGasCost = (
11✔
135
  item: Item,
136
  selectedToken: Token,
137
  chainNativeToken: Token | undefined,
138
  wallet: Wallet | null
139
): GasCost => {
140
  const chainId = parseInt(selectedToken.chainId) as ChainId
10✔
141

142
  const estimateGas = useCallback(
10✔
143
    () => (wallet ? estimateMintNftGas(chainId, wallet, item) : Promise.resolve(undefined)),
2!
144
    [chainId, wallet, item]
145
  )
146
  const shouldUseCrossChainProvider = useShouldUseCrossChainProvider(selectedToken, item.network)
10✔
147

148
  return useGasCost(item.network, chainNativeToken, chainId, shouldUseCrossChainProvider, wallet, estimateGas)
10✔
149
}
150

151
export const useBuyNftGasCost = (
11✔
152
  nft: NFT,
153
  order: Order,
154
  selectedToken: Token,
155
  chainNativeToken: Token | undefined,
156
  wallet: Wallet | null,
157
  fingerprint?: string
158
): GasCost => {
159
  const chainId = parseInt(selectedToken.chainId) as ChainId
10✔
160

161
  const estimateGas = useCallback(
10✔
162
    () =>
163
      wallet && (!nft.data.estate || (!!nft.data.estate && !!fingerprint))
2!
164
        ? estimateBuyNftGas(chainId, wallet, nft, order, fingerprint)
165
        : Promise.resolve(undefined),
166
    [chainId, wallet, order, nft, fingerprint]
167
  )
168
  const shouldUseCrossChainProvider = useShouldUseCrossChainProvider(selectedToken, order.network)
10✔
169

170
  return useGasCost(order.network, chainNativeToken, chainId, shouldUseCrossChainProvider, wallet, estimateGas)
10✔
171
}
172

173
export const useNameMintingGasCost = (name: string, selectedToken: Token, chainNativeToken: Token | undefined, wallet: Wallet | null) => {
11✔
174
  const chainId = parseInt(selectedToken.chainId) as ChainId
10✔
175

176
  const estimateGas = useCallback(
10✔
177
    () => (wallet?.address ? estimateNameMintingGas(name, chainId, wallet?.address) : Promise.resolve(undefined)),
2!
178
    [name, chainId, wallet?.address]
179
  )
180

181
  const shouldUseCrossChainProvider = useShouldUseCrossChainProvider(selectedToken, Network.ETHEREUM)
10✔
182

183
  return useGasCost(Network.ETHEREUM, chainNativeToken, chainId, shouldUseCrossChainProvider, wallet, estimateGas)
10✔
184
}
185

186
export const useCrossChainMintNftRoute = (
5✔
187
  item: Item,
188
  assetChainId: ChainId,
189
  selectedToken: Token,
190
  selectedChain: ChainId,
191
  providerTokens: Token[],
192
  crossChainProvider: CrossChainProvider | undefined,
193
  wallet: Wallet | null
194
) => {
195
  const getMintNFTRoute = useCallback(
×
196
    (fromAddress: string, fromAmount: string, fromChain: ChainId, fromToken: string, crossChainProvider: CrossChainProvider) =>
197
      crossChainProvider.getMintNFTRoute({
×
198
        fromAddress,
199
        fromAmount,
200
        fromToken,
201
        fromChain,
202
        toAmount: item.price,
203
        toChain: item.chainId,
204
        item: {
205
          collectionAddress: item.contractAddress,
206
          itemId: item.itemId,
207
          price: item.price,
208
          tradeId: item.tradeId
209
        },
210
        fetchTradeData: async () => {
211
          const trade = await new TradeService(API_SIGNER, MARKETPLACE_SERVER_URL, () => undefined).fetchTrade(item.tradeId as string)
×
212
          return {
×
213
            marketplaceAddress: trade.contract,
214
            onChainTrade: getOnChainTrade(trade, fromAddress)
215
          }
216
        }
217
      }),
218
    [item]
219
  )
220

221
  return useCrossChainRoute(
×
222
    item.price,
223
    assetChainId,
224
    selectedToken,
225
    selectedChain,
226
    providerTokens,
227
    crossChainProvider,
228
    wallet,
229
    getMintNFTRoute
230
  )
231
}
232

233
export const useCrossChainBuyNftRoute = (
5✔
234
  order: Order,
235
  assetChainId: ChainId,
236
  selectedToken: Token,
237
  selectedChain: ChainId,
238
  providerTokens: Token[],
239
  crossChainProvider: CrossChainProvider | undefined,
240
  wallet: Wallet | null,
241
  slippage: number
242
): CrossChainRoute => {
243
  const getBuyNftRoute = useCallback(
×
244
    (fromAddress: string, fromAmount: string, fromChain: ChainId, fromToken: string, crossChainProvider: CrossChainProvider) =>
245
      crossChainProvider.getBuyNFTRoute({
×
246
        fromAddress,
247
        fromAmount,
248
        fromChain,
249
        fromToken,
250
        toAmount: order.price,
251
        toChain: order.chainId,
252
        order,
253
        fetchTradeData:
254
          order.tradeId && wallet?.address
×
255
            ? async () => {
256
                const trade = await new TradeService(API_SIGNER, MARKETPLACE_SERVER_URL, () => undefined).fetchTrade(
×
257
                  order.tradeId as string
258
                )
259
                return {
×
260
                  marketplaceAddress: trade.contract,
261
                  onChainTrade: getOnChainTrade(trade, wallet.address)
262
                }
263
              }
264
            : undefined,
265
        slippage
266
      }),
267
    [order]
268
  )
269

270
  return useCrossChainRoute(
×
271
    order.price,
272
    assetChainId,
273
    selectedToken,
274
    selectedChain,
275
    providerTokens,
276
    crossChainProvider,
277
    wallet,
278
    getBuyNftRoute
279
  )
280
}
281

282
export const useCrossChainNameMintingRoute = (
5✔
283
  name: string,
284
  price: string,
285
  assetChainId: ChainId,
286
  selectedToken: Token,
287
  selectedChain: ChainId,
288
  providerTokens: Token[],
289
  crossChain: CrossChainProvider | undefined,
290
  wallet: Wallet | null,
291
  withCredits: boolean
292
) => {
293
  const getMintingNameRoute = useCallback(
×
294
    (fromAddress: string, fromAmount: string, fromChain: ChainId, fromToken: string, crossChainProvider: CrossChainProvider) =>
295
      crossChainProvider.getRegisterNameRoute({
×
296
        name,
297
        fromAddress,
298
        fromAmount,
299
        fromChain,
300
        fromToken,
301
        toAmount: price,
302
        toChain: assetChainId
303
      }),
304
    [name, assetChainId, price]
305
  )
NEW
306
  return useCrossChainRoute(
×
307
    price,
308
    assetChainId,
309
    selectedToken,
310
    selectedChain,
311
    providerTokens,
312
    crossChain,
313
    wallet,
314
    getMintingNameRoute,
315
    withCredits
316
  )
317
}
318

319
export type RouteFeeCost = {
320
  token: Token
321
  gasCostWei: BigNumber
322
  gasCost: string
323
  feeCost: string
324
  feeCostWei: BigNumber
325
  totalCost: string
326
}
327

328
export type CrossChainRoute = {
329
  route: Route | undefined
330
  fromAmount: string | undefined
331
  routeFeeCost: RouteFeeCost | undefined
332
  routeTotalUSDCost: number | undefined
333
  isFetchingRoute: boolean
334
  routeFailed: boolean
335
  refetchRoute?: () => void
336
}
337

338
const useCrossChainRoute = (
5✔
339
  price: string,
340
  assetChainId: ChainId,
341
  selectedToken: Token,
342
  selectedChain: ChainId,
343
  providerTokens: Token[],
344
  crossChainProvider: CrossChainProvider | undefined,
345
  wallet: Wallet | null,
346
  getRoute: (
347
    fromAddress: string,
348
    fromAmount: string,
349
    fromChain: ChainId,
350
    fromToken: string,
351
    crossChainProvider: CrossChainProvider
352
  ) => Promise<Route>,
353
  withCredits = false
×
354
): CrossChainRoute => {
355
  const [isFetchingRoute, setIsFetchingRoute] = useState(false)
×
356
  const [routeFailed, setRouteFailed] = useState(false)
×
357
  const [fromAmount, setFromAmount] = useState<string>()
×
358
  const [route, setRoute] = useState<Route>()
×
359
  const abortControllerRef = useRef(new AbortController())
×
360
  const destinationChainMANAContractAddress = useMemo(() => getContract(ContractName.MANAToken, assetChainId).address, [assetChainId])
×
361

362
  const calculateRoute = useCallback(async () => {
×
363
    abortControllerRef.current = new AbortController()
×
364
    const abortController = abortControllerRef.current
×
365
    const signal = abortController.signal
×
366
    const providerMANA = providerTokens.find(t => t.address.toLocaleLowerCase() === destinationChainMANAContractAddress.toLocaleLowerCase())
×
367
    if (!crossChainProvider || !crossChainProvider.isLibInitialized() || !wallet || !providerMANA) {
×
368
      return
×
369
    }
370
    try {
×
371
      setRoute(undefined)
×
372
      setIsFetchingRoute(true)
×
373
      setRouteFailed(false)
×
374
      const fromAmountParams = {
×
375
        fromToken: selectedToken,
376
        toAmount: ethers.utils.formatEther(price),
377
        toToken: providerMANA
378
      }
379
      const fromAmount = Number(await crossChainProvider.getFromAmount(fromAmountParams)).toFixed(6)
×
380
      setFromAmount(fromAmount)
×
381

382
      const fromAmountWei = ethers.utils.parseUnits(fromAmount.toString(), selectedToken.decimals).toString()
×
383

384
      const route: RouteResponse | undefined = await getRoute(
×
385
        wallet.address,
386
        fromAmountWei,
387
        selectedChain,
388
        selectedToken.address,
389
        crossChainProvider
390
      )
391

392
      if (route && !signal.aborted) {
×
393
        setRoute(route)
×
394
      }
395
    } catch (error) {
396
      console.error('Error while getting Route: ', error)
×
397
      getAnalytics()?.track(events.ERROR_GETTING_ROUTE, {
×
398
        error,
399
        selectedToken,
400
        selectedChain
401
      })
402
      setRouteFailed(true)
×
403
    } finally {
404
      setIsFetchingRoute(false)
×
405
    }
406
  }, [crossChainProvider, price, providerTokens, selectedChain, selectedToken, wallet])
407

408
  const useMetaTx = useMemo(() => {
×
409
    return (
×
410
      !!wallet &&
×
411
      getShouldUseMetaTx(assetChainId, selectedChain, selectedToken.address, destinationChainMANAContractAddress, wallet.network)
412
    )
413
  }, [destinationChainMANAContractAddress, selectedChain, selectedToken, wallet])
414

415
  // Refresh the route every ROUTE_FETCH_INTERVAL
416
  useEffect(() => {
×
417
    let interval: NodeJS.Timeout | undefined = undefined
×
418
    if (route) {
×
419
      if (interval) {
×
420
        clearInterval(interval)
×
421
      }
422

423
      interval = setInterval(() => {
×
424
        return calculateRoute()
×
425
      }, ROUTE_FETCH_INTERVAL)
426
    }
427
    return () => {
×
428
      if (interval) {
×
429
        clearInterval(interval)
×
430
      }
431
    }
432
  }, [calculateRoute, route])
433

434
  // Refresh the route every time the selected token changes
435
  useEffect(() => {
×
436
    // Abort previous request
437
    const abortController = abortControllerRef.current
×
438
    abortController.abort()
×
439

440
    if (!useMetaTx) {
×
441
      const isBuyingL1WithOtherTokenThanEthereumMANA =
442
        assetChainId === ChainId.ETHEREUM_MAINNET &&
×
443
        selectedToken.chainId !== ChainId.ETHEREUM_MAINNET.toString() &&
444
        selectedToken.symbol !== 'MANA'
445

446
      const isPayingWithOtherTokenThanMANA = selectedToken.symbol !== 'MANA'
×
447
      const isPayingWithMANAButFromOtherChain = selectedToken.symbol === 'MANA' && selectedToken.chainId !== assetChainId.toString()
×
448

449
      // The only cross-chain route to use credits is for NAMEs and it's calculated server-side
NEW
450
      if (
×
451
        (isBuyingL1WithOtherTokenThanEthereumMANA || isPayingWithOtherTokenThanMANA || isPayingWithMANAButFromOtherChain) &&
×
452
        !withCredits
453
      ) {
UNCOV
454
        void calculateRoute()
×
455
      }
456
    }
457
    setRouteFailed(false)
×
458
  }, [useMetaTx, selectedToken, selectedChain, assetChainId, calculateRoute])
459

460
  const routeFeeCost = useMemo(() => {
×
461
    if (route) {
×
462
      const {
463
        route: {
464
          estimate: { gasCosts, feeCosts }
465
        }
466
      } = route
×
467
      const totalGasCost = gasCosts.map(c => BigNumber.from(c.amount)).reduce((a, b) => a.add(b), BigNumber.from(0))
×
468
      const totalFeeCost = feeCosts.map(c => BigNumber.from(c.amount)).reduce((a, b) => a.add(b), BigNumber.from(0))
×
469
      const token = gasCosts[0].token
×
470
      return {
×
471
        token,
472
        gasCostWei: totalGasCost,
473
        gasCost: formatPrice(
474
          ethers.utils.formatUnits(totalGasCost, route.route.estimate.gasCosts[0].token.decimals),
475
          route.route.estimate.gasCosts[0].token
476
        ).toString(),
477
        feeCost: formatPrice(
478
          ethers.utils.formatUnits(totalFeeCost, route.route.estimate.gasCosts[0].token.decimals),
479
          route.route.estimate.gasCosts[0].token
480
        ).toString(),
481
        feeCostWei: totalFeeCost,
482
        totalCost: parseFloat(ethers.utils.formatUnits(totalGasCost.add(totalFeeCost), token.decimals)).toFixed(6)
483
      }
484
    }
485
  }, [route])
486

487
  const routeTotalUSDCost = useMemo(() => {
×
488
    if (route && routeFeeCost && fromAmount && selectedToken?.usdPrice) {
×
489
      const { feeCost, gasCost } = routeFeeCost
×
490
      const feeTokenUSDPrice = providerTokens.find(t => t.symbol === routeFeeCost.token.symbol)?.usdPrice
×
491
      return feeTokenUSDPrice
×
492
        ? feeTokenUSDPrice * (Number(gasCost) + Number(feeCost)) + selectedToken.usdPrice * Number(fromAmount)
493
        : undefined
494
    }
495
  }, [fromAmount, providerTokens, route, routeFeeCost, selectedToken.usdPrice])
496

497
  return {
×
498
    route,
499
    fromAmount,
500
    routeFeeCost,
501
    routeTotalUSDCost,
502
    isFetchingRoute,
503
    routeFailed
504
  }
505
}
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

© 2026 Coveralls, Inc