Skip to content
This repository was archived by the owner on Aug 30, 2022. It is now read-only.

Commit b03b97e

Browse files
Show helpful error messages when trying to wrap unapproved tokens (#423)
1 parent 5f601a9 commit b03b97e

File tree

6 files changed

+115
-20
lines changed

6 files changed

+115
-20
lines changed

etc/sdk.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,8 @@ export class Erc721<T extends Multiwrap_2 | SignatureDrop_2 | DropERC721 | Token
16941694
query: Erc721Supply | undefined;
16951695
// @internal
16961696
setApprovalForAll(operator: string, approved: boolean): Promise<TransactionResult>;
1697+
// @internal
1698+
setApprovalForToken(operator: string, tokenId: BigNumberish): Promise<TransactionResult>;
16971699
// (undocumented)
16981700
protected storage: IStorage;
16991701
transfer(to: string, tokenId: BigNumberish): Promise<TransactionResult>;

src/common/currency.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@ export async function approveErc20Allowance(
137137
}
138138
}
139139

140+
export async function hasERC20Allowance(
141+
contractToApprove: ContractWrapper<any>,
142+
currencyAddress: string,
143+
value: BigNumber,
144+
) {
145+
const provider = contractToApprove.getProvider();
146+
const erc20 = new ContractWrapper<IERC20>(
147+
provider,
148+
currencyAddress,
149+
ERC20Abi,
150+
{},
151+
);
152+
const owner = await contractToApprove.getSignerAddress();
153+
const spender = contractToApprove.readContract.address;
154+
const allowance = await erc20.readContract.allowance(owner, spender);
155+
return allowance.gte(value);
156+
}
157+
140158
export async function normalizeAmount(
141159
contractWrapper: ContractWrapper<BaseERC20>,
142160
amount: Amount,

src/common/marketplace.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,23 @@ import ERC721Abi from "../../abis/IERC721.json";
1919
import ERC165Abi from "../../abis/IERC165.json";
2020

2121
/**
22-
* This method checks if the given token is approved for the marketplace contract.
23-
* This is particularly useful for direct listings where the token
24-
* being listed may be moved before the listing is actually closed.
22+
* This method checks if the given token is approved for the transferrerContractAddress contract.
23+
* This is particularly useful for contracts that need to transfer NFTs on the users' behalf
2524
*
2625
* @internal
2726
* @param provider - The connected provider
28-
* @param marketplaceAddress - The address of the marketplace contract
27+
* @param transferrerContractAddress - The address of the marketplace contract
2928
* @param assetContract - The address of the asset contract.
3029
* @param tokenId - The token id of the token.
31-
* @param from - The address of the account that owns the token.
32-
* @returns - True if the marketplace is approved on the token, false otherwise.
30+
* @param owner - The address of the account that owns the token.
31+
* @returns - True if the transferrerContractAddress is approved on the token, false otherwise.
3332
*/
34-
export async function isTokenApprovedForMarketplace(
33+
export async function isTokenApprovedForTransfer(
3534
provider: providers.Provider,
36-
marketplaceAddress: string,
35+
transferrerContractAddress: string,
3736
assetContract: string,
3837
tokenId: BigNumberish,
39-
from: string,
38+
owner: string,
4039
): Promise<boolean> {
4140
try {
4241
const erc165 = new Contract(assetContract, ERC165Abi, provider) as IERC165;
@@ -45,21 +44,24 @@ export async function isTokenApprovedForMarketplace(
4544
if (isERC721) {
4645
const asset = new Contract(assetContract, ERC721Abi, provider) as IERC721;
4746

48-
const approved = await asset.isApprovedForAll(from, marketplaceAddress);
47+
const approved = await asset.isApprovedForAll(
48+
owner,
49+
transferrerContractAddress,
50+
);
4951
if (approved) {
5052
return true;
5153
}
5254
return (
5355
(await asset.getApproved(tokenId)).toLowerCase() ===
54-
marketplaceAddress.toLowerCase()
56+
transferrerContractAddress.toLowerCase()
5557
);
5658
} else if (isERC1155) {
5759
const asset = new Contract(
5860
assetContract,
5961
ERC1155Abi,
6062
provider,
6163
) as IERC1155;
62-
return await asset.isApprovedForAll(from, marketplaceAddress);
64+
return await asset.isApprovedForAll(owner, transferrerContractAddress);
6365
} else {
6466
console.error("Contract does not implement ERC 1155 or ERC 721.");
6567
return false;

src/contracts/multiwrap.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ import {
2323
TokensToWrap,
2424
WrappedTokens,
2525
} from "../types/multiwrap";
26-
import { normalizePriceValue } from "../common/currency";
26+
import { hasERC20Allowance, normalizePriceValue } from "../common/currency";
2727
import { ITokenBundle, TokensWrappedEvent } from "contracts/Multiwrap";
2828
import { MultiwrapContractSchema } from "../schema/contracts/multiwrap";
2929
import { BigNumberish, ethers } from "ethers";
3030
import TokenStruct = ITokenBundle.TokenStruct;
3131
import { QueryAllParams } from "../types";
32+
import { isTokenApprovedForTransfer } from "../common/marketplace";
3233

3334
/**
3435
* 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<MultiwrapContract> {
293294
const tokens: TokenStruct[] = [];
294295

295296
const provider = this.contractWrapper.getProvider();
297+
const owner = await this.contractWrapper.getSignerAddress();
296298

297299
if (contents.erc20Tokens) {
298300
for (const erc20 of contents.erc20Tokens) {
301+
const normalizedQuantity = await normalizePriceValue(
302+
provider,
303+
erc20.quantity,
304+
erc20.contractAddress,
305+
);
306+
const hasAllowance = await hasERC20Allowance(
307+
this.contractWrapper,
308+
erc20.contractAddress,
309+
normalizedQuantity,
310+
);
311+
if (!hasAllowance) {
312+
throw new Error(
313+
`ERC20 token with contract address "${
314+
erc20.contractAddress
315+
}" 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("${
316+
erc20.contractAddress
317+
}").setAllowance("${this.getAddress()}", ${erc20.quantity});\n\n`,
318+
);
319+
}
299320
tokens.push({
300321
assetContract: erc20.contractAddress,
301-
totalAmount: await normalizePriceValue(
302-
provider,
303-
erc20.quantity,
304-
erc20.contractAddress,
305-
),
322+
totalAmount: normalizedQuantity,
306323
tokenId: 0,
307324
tokenType: 0,
308325
});
@@ -311,6 +328,26 @@ export class Multiwrap extends Erc721<MultiwrapContract> {
311328

312329
if (contents.erc721Tokens) {
313330
for (const erc721 of contents.erc721Tokens) {
331+
const isApproved = await isTokenApprovedForTransfer(
332+
this.contractWrapper.getProvider(),
333+
this.getAddress(),
334+
erc721.contractAddress,
335+
erc721.tokenId,
336+
owner,
337+
);
338+
339+
if (!isApproved) {
340+
throw new Error(
341+
`ERC721 token "${erc721.tokenId}" with contract address "${
342+
erc721.contractAddress
343+
}" is not approved for transfer.\n\nYou can give approval the multiwrap contract to transfer this token by running:\n\nawait sdk.getNFTCollection("${
344+
erc721.contractAddress
345+
}").setApprovalForToken("${this.getAddress()}", ${
346+
erc721.tokenId
347+
});\n\n`,
348+
);
349+
}
350+
314351
tokens.push({
315352
assetContract: erc721.contractAddress,
316353
totalAmount: 0,
@@ -322,6 +359,23 @@ export class Multiwrap extends Erc721<MultiwrapContract> {
322359

323360
if (contents.erc1155Tokens) {
324361
for (const erc1155 of contents.erc1155Tokens) {
362+
const isApproved = await isTokenApprovedForTransfer(
363+
this.contractWrapper.getProvider(),
364+
this.getAddress(),
365+
erc1155.contractAddress,
366+
erc1155.tokenId,
367+
owner,
368+
);
369+
370+
if (!isApproved) {
371+
throw new Error(
372+
`ERC1155 token "${erc1155.tokenId}" with contract address "${
373+
erc1155.contractAddress
374+
}" is not approved for transfer.\n\nYou can give approval the multiwrap contract to transfer this token by running:\n\nawait sdk.getEdition("${
375+
erc1155.contractAddress
376+
}").setApprovalForAll("${this.getAddress()}", true);\n\n`,
377+
);
378+
}
325379
tokens.push({
326380
assetContract: erc1155.contractAddress,
327381
totalAmount: erc1155.quantity,

src/core/classes/erc-721.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,25 @@ export class Erc721<
199199
};
200200
}
201201

202+
/**
203+
* Approve an operator for the NFT owner. Operators can call transferFrom or safeTransferFrom for the specified token.
204+
* @param operator - the operator's address
205+
* @param tokenId - the tokenId to give approval for
206+
*
207+
* @internal
208+
*/
209+
public async setApprovalForToken(
210+
operator: string,
211+
tokenId: BigNumberish,
212+
): Promise<TransactionResult> {
213+
return {
214+
receipt: await this.contractWrapper.sendTransaction("approve", [
215+
operator,
216+
tokenId,
217+
]),
218+
};
219+
}
220+
202221
/** ******************************
203222
* PRIVATE FUNCTIONS
204223
*******************************/

src/core/classes/marketplace-direct.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
} from "../../constants/contract";
3737
import {
3838
handleTokenApproval,
39-
isTokenApprovedForMarketplace,
39+
isTokenApprovedForTransfer,
4040
mapOffer,
4141
validateNewListingParam,
4242
} from "../../common/marketplace";
@@ -465,7 +465,7 @@ export class MarketplaceDirect {
465465
listing: DirectListing,
466466
quantity?: BigNumberish,
467467
): Promise<boolean> {
468-
const approved = await isTokenApprovedForMarketplace(
468+
const approved = await isTokenApprovedForTransfer(
469469
this.contractWrapper.getProvider(),
470470
this.getAddress(),
471471
listing.assetContractAddress,

0 commit comments

Comments
 (0)