Skip to content

Commit

Permalink
feat: create bids trades (#2265)
Browse files Browse the repository at this point in the history
  • Loading branch information
Melisa Anabella Rossi authored Jul 9, 2024
1 parent 47b4d7d commit 116f791
Show file tree
Hide file tree
Showing 29 changed files with 1,120 additions and 188 deletions.
16 changes: 8 additions & 8 deletions webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@0xsquid/squid-types": "^0.1.29",
"@covalenthq/client-sdk": "^0.6.4",
"@dcl/crypto": "^3.0.0",
"@dcl/schemas": "^11.7.0",
"@dcl/schemas": "^11.10.5",
"@dcl/single-sign-on-client": "^0.1.0",
"@dcl/ui-env": "^1.5.0",
"@ethersproject/providers": "^5.6.2",
Expand All @@ -23,7 +23,7 @@
"decentraland-connect": "^6.3.1",
"decentraland-crypto-fetch": "^1.0.3",
"decentraland-dapps": "^23.1.0",
"decentraland-transactions": "^2.7.0",
"decentraland-transactions": "^2.9.0",
"decentraland-ui": "^6.1.1",
"ethers": "^5.6.8",
"graphql": "^14.7.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { connect } from 'react-redux'
import { openModal } from 'decentraland-dapps/dist/modules/modal/actions'
import { getIsBidsOffChainEnabled } from '../../../../modules/features/selectors'
import { RootState } from '../../../../modules/reducer'
import { getWallet } from '../../../../modules/wallet/selectors'
import ItemSaleActions from './ItemSaleActions'
Expand All @@ -9,7 +10,8 @@ const mapState = (state: RootState): MapStateProps => {
const wallet = getWallet(state)

return {
wallet
wallet,
isBidsOffchainEnabled: getIsBidsOffChainEnabled(state)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
flex-direction: column;
gap: 8px;
}

.bidButton:global(.ui.button) {
background-color: var(--superGray);
}

.bidButton:global(.ui.button):hover {
background-color: var(--superGrayHovered);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { t } from 'decentraland-dapps/dist/modules/translation'
import { Wallet } from 'decentraland-dapps/dist/modules/wallet'
import { Item } from '../../../../modules/item/types'
import { renderWithProviders } from '../../../../utils/test'
import ItemSaleActions from './ItemSaleActions'
import { Props } from './ItemSaleActions.types'

function renderItemSaleActions(props: Partial<Props> = {}) {
return renderWithProviders(
<ItemSaleActions
item={{ id: 'dasd', available: 2, creator: '0xcreator' } as Item}
wallet={{ address: '0xtest' } as Wallet}
isBidsOffchainEnabled={false}
onBuyWithCrypto={jest.fn()}
{...props}
/>
)
}

let props: Partial<Props> = {}

describe('when off chain bids are enabled', () => {
beforeEach(() => {
props = { ...props, isBidsOffchainEnabled: true }
})

describe('and the user is not the creator of the item', () => {
beforeEach(() => {
props = { ...props, wallet: { address: '0xuser' } as Wallet, item: { id: '1', creator: '0xcreator' } as Item }
})

describe('and slots available to mint', () => {
beforeEach(() => {
props = { ...props, item: { ...props.item, available: 2 } as Item }
})

it('should render the bid button', () => {
const { getByRole } = renderItemSaleActions(props)
expect(getByRole('link', { name: t('asset_page.actions.place_bid') })).toBeInTheDocument()
})
})

describe('and there are no slots available to mint', () => {
beforeEach(() => {
props = { ...props, item: { ...props.item, available: 0 } as Item }
})

it('should not render the bid button', () => {
const { queryByRole } = renderItemSaleActions(props)
expect(queryByRole('link', { name: t('asset_page.actions.place_bid') })).not.toBeInTheDocument()
})
})
})

describe('and the user is the creator of the item', () => {
beforeEach(() => {
props = { ...props, wallet: { address: '0xcreator' } as Wallet, item: { id: '1', creator: '0xcreator' } as Item }
})

it('should not render the bid button', () => {
const { queryByRole } = renderItemSaleActions(props)
expect(queryByRole('link', { name: t('asset_page.actions.place_bid') })).not.toBeInTheDocument()
})
})
})

describe('when off chain bids are disabled', () => {
beforeEach(() => {
props = { ...props, isBidsOffchainEnabled: false }
})

it('should not render the bid button', () => {
const { queryByRole } = renderItemSaleActions(props)
expect(queryByRole('link', { name: t('asset_page.actions.place_bid') })).not.toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { memo } from 'react'
import { Link } from 'react-router-dom'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { Button } from 'decentraland-ui'
import { AssetType } from '../../../../modules/asset/types'
import { getBuilderCollectionDetailUrl } from '../../../../modules/collection/utils'
import { locations } from '../../../../modules/routing/locations'
import { BuyNFTButtons } from '../BuyNFTButtons'
import { Props } from './ItemSaleActions.types'
import styles from './ItemSaleActions.module.css'

const ItemSaleActions = ({ item, wallet, customClassnames }: Props) => {
const ItemSaleActions = ({ item, wallet, isBidsOffchainEnabled, customClassnames }: Props) => {
const isOwner = wallet?.address === item.creator
const canBuy = !isOwner && item.isOnSale && item.available > 0
const canBid = isBidsOffchainEnabled && !isOwner && item.available > 0
const builderCollectionUrl = getBuilderCollectionDetailUrl(item.contractAddress)

return (
Expand All @@ -27,7 +30,16 @@ const ItemSaleActions = ({ item, wallet, customClassnames }: Props) => {
</Button>
</div>
) : (
canBuy && <BuyNFTButtons asset={item} assetType={AssetType.ITEM} buyWithCardClassName={customClassnames?.buyWithCardClassName} />
<>
{canBuy && (
<BuyNFTButtons asset={item} assetType={AssetType.ITEM} buyWithCardClassName={customClassnames?.buyWithCardClassName} />
)}
{canBid && (
<Button as={Link} role="link" to={locations.bidItem(item.contractAddress, item.itemId)} className={styles.bidButton}>
{t('asset_page.actions.place_bid')}
</Button>
)}
</>
)}
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types'
export type Props = {
item: Item
wallet?: Wallet | null
isBidsOffchainEnabled: boolean
onBuyWithCrypto: () => void
customClassnames?: { [key: string]: string } | undefined
}

export type OwnProps = Pick<Props, 'item' | 'customClassnames'>
export type MapStateProps = Pick<Props, 'wallet'>
export type MapStateProps = Pick<Props, 'wallet' | 'isBidsOffchainEnabled'>

export type MapDispatchProps = Pick<Props, 'onBuyWithCrypto'>
export type MapDispatch = Dispatch<OpenModalAction>
54 changes: 30 additions & 24 deletions webapp/src/components/BidPage/BidModal/BidModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { AuthorizedAction } from 'decentraland-dapps/dist/containers/withAuthori
import { toFixedMANAValue } from 'decentraland-dapps/dist/lib/mana'
import { AuthorizationType } from 'decentraland-dapps/dist/modules/authorization/types'
import { t, T } from 'decentraland-dapps/dist/modules/translation/utils'
import { ContractName } from 'decentraland-transactions'
import { ContractName, getContract as getDecentralandContract } from 'decentraland-transactions'
import { Header, Form, Field, Button } from 'decentraland-ui'
import { parseMANANumber } from '../../../lib/mana'
import { getAssetName, isOwnedBy } from '../../../modules/asset/utils'
import { getAssetName, isNFT, isOwnedBy } from '../../../modules/asset/utils'
import { getBidStatus, getError } from '../../../modules/bid/selectors'
import { useFingerprint } from '../../../modules/nft/hooks'
import { isLand } from '../../../modules/nft/utils'
Expand All @@ -26,32 +26,34 @@ import { Props } from './BidModal.types'
import './BidModal.css'

const BidModal = (props: Props) => {
const { nft, rental, wallet, onNavigate, onPlaceBid, isPlacingBid, isLoadingAuthorization, getContract } = props
const { asset, rental, wallet, onNavigate, onPlaceBid, isPlacingBid, isLoadingAuthorization, isBidsOffchainEnabled, getContract } = props

const [price, setPrice] = useState('')
const [expiresAt, setExpiresAt] = useState(getDefaultExpirationDate())

const [fingerprint, isLoadingFingerprint, contractFingerprint] = useFingerprint(nft)
const [fingerprint, isLoadingFingerprint, contractFingerprint] = useFingerprint(isNFT(asset) ? asset : null)

const [showConfirmationModal, setShowConfirmationModal] = useState(false)

const handlePlaceBid = useCallback(
() => onPlaceBid(nft, parseMANANumber(price), +new Date(`${expiresAt} 00:00:00`), fingerprint),
[nft, price, expiresAt, fingerprint, onPlaceBid]
() => onPlaceBid(asset, parseMANANumber(price), +new Date(`${expiresAt} 00:00:00`), fingerprint),
[asset, price, expiresAt, fingerprint, onPlaceBid]
)

const contractNames = getContractNames()

const mana = getContract({
name: contractNames.MANA,
network: nft.network
network: asset.network
})

const bids = getContract({
name: contractNames.BIDS,
network: nft.network
network: asset.network
})

const offchainBidsContract = getDecentralandContract(ContractName.OffChainMarketplace, asset.chainId)

if (!wallet || !mana || !bids) {
return null
}
Expand All @@ -65,48 +67,48 @@ const BidModal = (props: Props) => {
onClearBidError()
onAuthorizedAction({
targetContractName: ContractName.MANAToken,
authorizedAddress: bids.address,
authorizedAddress: isBidsOffchainEnabled ? offchainBidsContract.address : bids.address,
targetContract: mana as Contract,
authorizationType: AuthorizationType.ALLOWANCE,
authorizedContractLabel: bids.label || bids.name,
authorizedContractLabel: isBidsOffchainEnabled ? offchainBidsContract.name : bids.label || bids.name,
requiredAllowanceInWei: ethers.utils.parseEther(price).toString(),
onAuthorized: handlePlaceBid
})
}

const isInvalidPrice = parseMANANumber(price) <= 0
const isInvalidDate = +new Date(`${expiresAt} 00:00:00`) < Date.now()
const hasInsufficientMANA = !!price && !!wallet && parseMANANumber(price) > wallet.networks[nft.network].mana
const hasInsufficientMANA = !!price && !!wallet && parseMANANumber(price) > wallet.networks[asset.network].mana

const isDisabled =
isOwnedBy(nft, wallet) ||
isOwnedBy(asset, wallet) ||
isInvalidPrice ||
isInvalidDate ||
hasInsufficientMANA ||
isLoadingFingerprint ||
isPlacingBid ||
(!fingerprint && nft.category === NFTCategory.ESTATE) ||
(!fingerprint && asset.category === NFTCategory.ESTATE) ||
contractFingerprint !== fingerprint

return (
<AssetAction asset={nft}>
<AssetAction asset={asset}>
<div className="bid-action">
<Header size="large">{t('bid_page.title')}</Header>
<p className="subtitle">
<T
id="bid_page.subtitle"
values={{
name: <b className="primary-text">{getAssetName(nft)}</b>
name: <b className="primary-text">{getAssetName(asset)}</b>
}}
/>
</p>
{isLand(nft) && rental && isRentalListingExecuted(rental) && !hasRentalEnded(rental) ? (
{isLand(asset) && rental && isRentalListingExecuted(rental) && !hasRentalEnded(rental) ? (
<div className="rentalMessage">
<T
id="bid_page.rental_executed"
values={{
rental_end_date: getRentalEndDate(rental),
asset_type: nft.category,
asset_type: asset.category,
strong: (children: any) => <strong>{children}</strong>
}}
/>
Expand All @@ -115,7 +117,7 @@ const BidModal = (props: Props) => {
<Form onSubmit={handleSubmit}>
<div className="form-fields">
<ManaField
network={nft.network}
network={asset.network}
label={t('bid_page.price')}
placeholder={1000}
value={price}
Expand All @@ -126,7 +128,7 @@ const BidModal = (props: Props) => {
message={hasInsufficientMANA ? t('bid_page.not_enougn_mana') : undefined}
/>
<Field
network={nft.network}
network={asset.network}
label={t('bid_page.expiration_date')}
type="date"
value={expiresAt}
Expand All @@ -142,11 +144,15 @@ const BidModal = (props: Props) => {
<Button
as="div"
disabled={isLoadingFingerprint || isPlacingBid}
onClick={() => onNavigate(locations.nft(nft.contractAddress, nft.tokenId))}
onClick={() =>
onNavigate(
isNFT(asset) ? locations.nft(asset.contractAddress, asset.tokenId) : locations.item(asset.contractAddress, asset.itemId)
)
}
>
{t('global.cancel')}
</Button>
<ChainButton type="submit" primary loading={isPlacingBid} disabled={isDisabled} chainId={nft.chainId}>
<ChainButton type="submit" primary loading={isPlacingBid} disabled={isDisabled} chainId={asset.chainId}>
{t('bid_page.submit')}
</ChainButton>
</div>
Expand All @@ -160,9 +166,9 @@ const BidModal = (props: Props) => {
<T
id="bid_page.confirm.create_bid_line_one"
values={{
name: <b>{getAssetName(nft)}</b>,
name: <b>{getAssetName(asset)}</b>,
amount: (
<Mana showTooltip network={nft.network} inline>
<Mana showTooltip network={asset.network} inline>
{parseMANANumber(price).toLocaleString()}
</Mana>
)
Expand All @@ -174,7 +180,7 @@ const BidModal = (props: Props) => {
}
onConfirm={handleConfirmBid}
valueToConfirm={price}
network={nft.network}
network={asset.network}
onCancel={() => setShowConfirmationModal(false)}
loading={isPlacingBid || isLoadingAuthorization}
disabled={isPlacingBid || isLoadingAuthorization}
Expand Down
Loading

0 comments on commit 116f791

Please sign in to comment.