diff --git a/src/logic/trades/utils.ts b/src/logic/trades/utils.ts index f2f342a..b2f8f6f 100644 --- a/src/logic/trades/utils.ts +++ b/src/logic/trades/utils.ts @@ -1,6 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { TypedDataField, hexlify, TypedDataDomain, verifyTypedData, toBeArray, toUtf8Bytes, zeroPadValue } from 'ethers' -import { TradeAsset, TradeAssetType, TradeCreation } from '@dcl/schemas' +import { + Contract, + TypedDataField, + hexlify, + TypedDataDomain, + verifyTypedData, + toBeArray, + toUtf8Bytes, + zeroPadValue, + JsonRpcProvider +} from 'ethers' +import { ChainId, ERC721TradeAsset, Network, TradeAsset, TradeAssetType, TradeCreation } from '@dcl/schemas' import { ContractData, ContractName, getContract } from 'decentraland-transactions' import { InvalidECDSASignatureError, MarketplaceContractNotFound } from '../../ports/trades/errors' import { fromMillisecondsToSeconds } from '../date' @@ -109,3 +119,34 @@ export function validateTradeSignature(trade: TradeCreation, signer: string): bo return verifyTypedData(domain, MARKETPLACE_TRADE_TYPES, values, trade.signature).toLowerCase() === signer } + +export function isERC721TradeAsset(asset: TradeAsset): asset is ERC721TradeAsset { + return (asset as ERC721TradeAsset).tokenId !== undefined +} + +async function getContractOwner(contractAddress: string, tokenId: string, network: Network, chainId: ChainId): Promise { + const abi = ['function ownerOf(uint256 tokenId) view returns (address)'] + const RPC_URL = `https://rpc.decentraland.org/${ + network === Network.ETHEREUM + ? chainId === ChainId.ETHEREUM_MAINNET + ? 'mainnet' + : 'sepolia' + : chainId === ChainId.MATIC_MAINNET + ? 'polygon' + : 'amoy' + }` + const provider = new JsonRpcProvider(RPC_URL) + const contract = new Contract(contractAddress, abi, provider) + return await contract.ownerOf(tokenId) +} + +export async function validateAssetOwnership( + asset: ERC721TradeAsset, + signer: string, + network: Network, + chainId: ChainId +): Promise { + const { contractAddress, tokenId } = asset + const blockchainOwner = await getContractOwner(contractAddress, tokenId, network, chainId) + return blockchainOwner.toLowerCase() === signer.toLowerCase() +} diff --git a/src/ports/trades/component.ts b/src/ports/trades/component.ts index f221df4..162e6b0 100644 --- a/src/ports/trades/component.ts +++ b/src/ports/trades/component.ts @@ -2,7 +2,7 @@ import SQL from 'sql-template-strings' import { Event, TradeAssetDirection, TradeCreation } from '@dcl/schemas' import { fromDbTradeAndDBTradeAssetWithValueListToTrade } from '../../adapters/trades/trades' import { isErrorWithMessage } from '../../logic/errors' -import { validateTradeSignature } from '../../logic/trades/utils' +import { validateAssetOwnership, validateTradeSignature } from '../../logic/trades/utils' import { AppComponents } from '../../types' import { InvalidTradeSignatureError, @@ -12,7 +12,8 @@ import { InvalidTradeSignerError, TradeNotFoundError, EventNotGeneratedError, - TradeNotFoundBySignatureError + TradeNotFoundBySignatureError, + InvalidOwnerError } from './errors' import { getInsertTradeAssetQuery, @@ -22,7 +23,7 @@ import { getTradeAssetsWithValuesByIdQuery } from './queries' import { DBTrade, DBTradeAsset, DBTradeAssetValue, DBTradeAssetWithValue, ITradesComponent, TradeEvent } from './types' -import { getNotificationEventForTrade, validateTradeByType } from './utils' +import { getNotificationEventForTrade, isERC721TradeAsset, validateTradeByType } from './utils' export function createTradesComponent(components: Pick): ITradesComponent { const { dappsDatabase: pg, eventPublisher, logs } = components @@ -58,6 +59,11 @@ export function createTradesComponent(components: Pick { const insertedTrade = await client.query(getInsertTradeQuery(trade, signer)) diff --git a/src/ports/trades/errors.ts b/src/ports/trades/errors.ts index 0cfca7c..a1f7b50 100644 --- a/src/ports/trades/errors.ts +++ b/src/ports/trades/errors.ts @@ -30,6 +30,12 @@ export class InvalidTradeSignatureError extends Error { } } +export class InvalidOwnerError extends Error { + constructor() { + super('Invalid owner') + } +} + export class MarketplaceContractNotFound extends Error { constructor(public chainId: ChainId, public network: Network) { super(`Contract not found for ${chainId} and ${network}`)