From 083ca235bb95c45df236f70000e9634bae208a89 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Fri, 27 May 2022 13:57:30 -0700 Subject: [PATCH] Show helpful error messages when trying to wrap unapproved tokens --- etc/sdk.api.md | 2 + src/common/currency.ts | 18 +++++++ src/common/marketplace.ts | 26 +++++----- src/contracts/multiwrap.ts | 66 +++++++++++++++++++++++--- src/core/classes/erc-721.ts | 19 ++++++++ src/core/classes/marketplace-direct.ts | 4 +- 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/etc/sdk.api.md b/etc/sdk.api.md index c24dcbece..cd1fe82c8 100644 --- a/etc/sdk.api.md +++ b/etc/sdk.api.md @@ -1694,6 +1694,8 @@ export class Erc721; + // @internal + setApprovalForToken(operator: string, tokenId: BigNumberish): Promise; // (undocumented) protected storage: IStorage; transfer(to: string, tokenId: BigNumberish): Promise; diff --git a/src/common/currency.ts b/src/common/currency.ts index e3d690419..9ec3a55e2 100644 --- a/src/common/currency.ts +++ b/src/common/currency.ts @@ -137,6 +137,24 @@ export async function approveErc20Allowance( } } +export async function hasERC20Allowance( + contractToApprove: ContractWrapper, + currencyAddress: string, + value: BigNumber, +) { + const provider = contractToApprove.getProvider(); + const erc20 = new ContractWrapper( + provider, + currencyAddress, + ERC20Abi, + {}, + ); + const owner = await contractToApprove.getSignerAddress(); + const spender = contractToApprove.readContract.address; + const allowance = await erc20.readContract.allowance(owner, spender); + return allowance.gte(value); +} + export async function normalizeAmount( contractWrapper: ContractWrapper, amount: Amount, diff --git a/src/common/marketplace.ts b/src/common/marketplace.ts index 07580e61a..9dfc8eeda 100644 --- a/src/common/marketplace.ts +++ b/src/common/marketplace.ts @@ -19,24 +19,23 @@ import ERC721Abi from "../../abis/IERC721.json"; import ERC165Abi from "../../abis/IERC165.json"; /** - * This method checks if the given token is approved for the marketplace contract. - * This is particularly useful for direct listings where the token - * being listed may be moved before the listing is actually closed. + * This method checks if the given token is approved for the transferrerContractAddress contract. + * This is particularly useful for contracts that need to transfer NFTs on the users' behalf * * @internal * @param provider - The connected provider - * @param marketplaceAddress - The address of the marketplace contract + * @param transferrerContractAddress - The address of the marketplace contract * @param assetContract - The address of the asset contract. * @param tokenId - The token id of the token. - * @param from - The address of the account that owns the token. - * @returns - True if the marketplace is approved on the token, false otherwise. + * @param owner - The address of the account that owns the token. + * @returns - True if the transferrerContractAddress is approved on the token, false otherwise. */ -export async function isTokenApprovedForMarketplace( +export async function isTokenApprovedForTransfer( provider: providers.Provider, - marketplaceAddress: string, + transferrerContractAddress: string, assetContract: string, tokenId: BigNumberish, - from: string, + owner: string, ): Promise { try { const erc165 = new Contract(assetContract, ERC165Abi, provider) as IERC165; @@ -45,13 +44,16 @@ export async function isTokenApprovedForMarketplace( if (isERC721) { const asset = new Contract(assetContract, ERC721Abi, provider) as IERC721; - const approved = await asset.isApprovedForAll(from, marketplaceAddress); + const approved = await asset.isApprovedForAll( + owner, + transferrerContractAddress, + ); if (approved) { return true; } return ( (await asset.getApproved(tokenId)).toLowerCase() === - marketplaceAddress.toLowerCase() + transferrerContractAddress.toLowerCase() ); } else if (isERC1155) { const asset = new Contract( @@ -59,7 +61,7 @@ export async function isTokenApprovedForMarketplace( ERC1155Abi, provider, ) as IERC1155; - return await asset.isApprovedForAll(from, marketplaceAddress); + return await asset.isApprovedForAll(owner, transferrerContractAddress); } else { console.error("Contract does not implement ERC 1155 or ERC 721."); return false; diff --git a/src/contracts/multiwrap.ts b/src/contracts/multiwrap.ts index b16771ef3..581032bcd 100644 --- a/src/contracts/multiwrap.ts +++ b/src/contracts/multiwrap.ts @@ -23,12 +23,13 @@ import { TokensToWrap, WrappedTokens, } from "../types/multiwrap"; -import { normalizePriceValue } from "../common/currency"; +import { hasERC20Allowance, normalizePriceValue } from "../common/currency"; import { ITokenBundle, TokensWrappedEvent } from "contracts/Multiwrap"; import { MultiwrapContractSchema } from "../schema/contracts/multiwrap"; import { BigNumberish, ethers } from "ethers"; import TokenStruct = ITokenBundle.TokenStruct; import { QueryAllParams } from "../types"; +import { isTokenApprovedForTransfer } from "../common/marketplace"; /** * Multiwrap lets you wrap any number of ERC20, ERC721 and ERC1155 tokens you own into a single wrapped token bundle. @@ -293,16 +294,32 @@ export class Multiwrap extends Erc721 { const tokens: TokenStruct[] = []; const provider = this.contractWrapper.getProvider(); + const owner = await this.contractWrapper.getSignerAddress(); if (contents.erc20Tokens) { for (const erc20 of contents.erc20Tokens) { + const normalizedQuantity = await normalizePriceValue( + provider, + erc20.quantity, + erc20.contractAddress, + ); + const hasAllowance = await hasERC20Allowance( + this.contractWrapper, + erc20.contractAddress, + normalizedQuantity, + ); + if (!hasAllowance) { + throw new Error( + `ERC20 token with contract address "${ + erc20.contractAddress + }" does not have enough allowance to transfer.\n\nYou can set allowance to the multiwrap contract to transfer these tokens by running:\n\nawait sdk.getToken("${ + erc20.contractAddress + }").setAllowance("${this.getAddress()}", ${erc20.quantity});\n\n`, + ); + } tokens.push({ assetContract: erc20.contractAddress, - totalAmount: await normalizePriceValue( - provider, - erc20.quantity, - erc20.contractAddress, - ), + totalAmount: normalizedQuantity, tokenId: 0, tokenType: 0, }); @@ -311,6 +328,26 @@ export class Multiwrap extends Erc721 { if (contents.erc721Tokens) { for (const erc721 of contents.erc721Tokens) { + const isApproved = await isTokenApprovedForTransfer( + this.contractWrapper.getProvider(), + this.getAddress(), + erc721.contractAddress, + erc721.tokenId, + owner, + ); + + if (!isApproved) { + throw new Error( + `ERC721 token "${erc721.tokenId}" with contract address "${ + erc721.contractAddress + }" is not approved for transfer.\n\nYou can give approval the multiwrap contract to transfer this token by running:\n\nawait sdk.getNFTCollection("${ + erc721.contractAddress + }").setApprovalForToken("${this.getAddress()}", ${ + erc721.tokenId + });\n\n`, + ); + } + tokens.push({ assetContract: erc721.contractAddress, totalAmount: 0, @@ -322,6 +359,23 @@ export class Multiwrap extends Erc721 { if (contents.erc1155Tokens) { for (const erc1155 of contents.erc1155Tokens) { + const isApproved = await isTokenApprovedForTransfer( + this.contractWrapper.getProvider(), + this.getAddress(), + erc1155.contractAddress, + erc1155.tokenId, + owner, + ); + + if (!isApproved) { + throw new Error( + `ERC1155 token "${erc1155.tokenId}" with contract address "${ + erc1155.contractAddress + }" is not approved for transfer.\n\nYou can give approval the multiwrap contract to transfer this token by running:\n\nawait sdk.getEdition("${ + erc1155.contractAddress + }").setApprovalForAll("${this.getAddress()}", true);\n\n`, + ); + } tokens.push({ assetContract: erc1155.contractAddress, totalAmount: erc1155.quantity, diff --git a/src/core/classes/erc-721.ts b/src/core/classes/erc-721.ts index f54303545..24902c589 100644 --- a/src/core/classes/erc-721.ts +++ b/src/core/classes/erc-721.ts @@ -199,6 +199,25 @@ export class Erc721< }; } + /** + * Approve an operator for the NFT owner. Operators can call transferFrom or safeTransferFrom for the specified token. + * @param operator - the operator's address + * @param tokenId - the tokenId to give approval for + * + * @internal + */ + public async setApprovalForToken( + operator: string, + tokenId: BigNumberish, + ): Promise { + return { + receipt: await this.contractWrapper.sendTransaction("approve", [ + operator, + tokenId, + ]), + }; + } + /** ****************************** * PRIVATE FUNCTIONS *******************************/ diff --git a/src/core/classes/marketplace-direct.ts b/src/core/classes/marketplace-direct.ts index c10cd277d..355cd4b98 100644 --- a/src/core/classes/marketplace-direct.ts +++ b/src/core/classes/marketplace-direct.ts @@ -36,7 +36,7 @@ import { } from "../../constants/contract"; import { handleTokenApproval, - isTokenApprovedForMarketplace, + isTokenApprovedForTransfer, mapOffer, validateNewListingParam, } from "../../common/marketplace"; @@ -465,7 +465,7 @@ export class MarketplaceDirect { listing: DirectListing, quantity?: BigNumberish, ): Promise { - const approved = await isTokenApprovedForMarketplace( + const approved = await isTokenApprovedForTransfer( this.contractWrapper.getProvider(), this.getAddress(), listing.assetContractAddress,