UNPKG

opensea-js

Version:

TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data

398 lines 19.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AssetsManager = void 0; const ethers_1 = require("ethers"); const constants_1 = require("../constants"); const contracts_1 = require("../typechain/contracts"); const types_1 = require("../types"); const utils_1 = require("../utils/utils"); /** * Asset transfer and approval operations */ class AssetsManager { constructor(context) { this.context = context; } /** * Get an account's balance of any Asset. This asset can be an ERC20, ERC1155, or ERC721. * @param options * @param options.accountAddress Account address to check * @param options.asset The Asset to check balance for. tokenStandard must be set. * @returns The balance of the asset for the account. * * @throws Error if the token standard does not support balanceOf. */ async getBalance({ accountAddress, asset, }) { switch (asset.tokenStandard) { case types_1.TokenStandard.ERC20: { const contract = contracts_1.ERC20__factory.connect(asset.tokenAddress, this.context.provider); return await contract.balanceOf.staticCall(accountAddress); } case types_1.TokenStandard.ERC1155: { if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC1155 tokenId for getBalance"); } const contract = contracts_1.ERC1155__factory.connect(asset.tokenAddress, this.context.provider); return await contract.balanceOf.staticCall(accountAddress, asset.tokenId); } case types_1.TokenStandard.ERC721: { if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC721 tokenId for getBalance"); } const contract = contracts_1.ERC721__factory.connect(asset.tokenAddress, this.context.provider); try { const owner = await contract.ownerOf.staticCall(asset.tokenId); return BigInt(owner.toLowerCase() == accountAddress.toLowerCase()); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) { this.context.logger(`Failed to get ownerOf ERC721: ${error.message ?? error}`); return 0n; } } default: throw new Error("Unsupported token standard for getBalance"); } } /** * Transfer an asset. This asset can be an ERC20, ERC1155, or ERC721. * @param options * @param options.asset The Asset to transfer. tokenStandard must be set. * @param options.amount Amount of asset to transfer. Not used for ERC721. * @param options.fromAddress The address to transfer from * @param options.toAddress The address to transfer to * @param options.overrides Transaction overrides, ignored if not set. */ async transfer({ asset, amount, fromAddress, toAddress, overrides, }) { overrides = { ...overrides, from: fromAddress }; let transaction; switch (asset.tokenStandard) { case types_1.TokenStandard.ERC20: { if (!amount) { throw new Error("Missing ERC20 amount for transfer"); } const contract = contracts_1.ERC20__factory.connect(asset.tokenAddress, this.context.signerOrProvider); transaction = contract.transfer(toAddress, amount, overrides); break; } case types_1.TokenStandard.ERC1155: { if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC1155 tokenId for transfer"); } if (!amount) { throw new Error("Missing ERC1155 amount for transfer"); } const contract = contracts_1.ERC1155__factory.connect(asset.tokenAddress, this.context.signerOrProvider); transaction = contract.safeTransferFrom(fromAddress, toAddress, asset.tokenId, amount, "0x", overrides); break; } case types_1.TokenStandard.ERC721: { if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC721 tokenId for transfer"); } const contract = contracts_1.ERC721__factory.connect(asset.tokenAddress, this.context.signerOrProvider); transaction = contract.transferFrom(fromAddress, toAddress, asset.tokenId, overrides); break; } default: throw new Error("Unsupported token standard for transfer"); } try { const transactionResponse = await transaction; await this.context.confirmTransaction(transactionResponse.hash, types_1.EventType.Transfer, "Transferring asset"); } catch (error) { console.error(error); this.context.dispatch(types_1.EventType.TransactionDenied, { error, accountAddress: fromAddress, }); } } /** * Bulk transfer multiple assets using OpenSea's TransferHelper contract. * This method is more gas-efficient than calling transfer() multiple times. * Note: All assets must be approved for transfer to the OpenSea conduit before calling this method. * @param options * @param options.assets Array of assets to transfer. Each asset must have tokenStandard set. * @param options.fromAddress The address to transfer from * @param options.overrides Transaction overrides, ignored if not set. * @returns Transaction hash of the bulk transfer * * @throws Error if any asset is missing required fields (tokenId for NFTs, amount for ERC20/ERC1155). * @throws Error if any asset is not approved for transfer to the OpenSea conduit. * @throws Error if the fromAddress is not available through wallet or provider. */ async bulkTransfer({ assets, fromAddress, overrides, }) { // Validate basic parameters before making any blockchain calls if (assets.length === 0) { throw new Error("At least one asset must be provided"); } // Validate asset data and build transfer items array for TransferHelper // This validation happens before any blockchain calls to ensure proper error messages const transferItems = []; for (const { asset, toAddress, amount } of assets) { let itemType; let identifier; let transferAmount; switch (asset.tokenStandard) { case types_1.TokenStandard.ERC20: itemType = 1; // ERC20 identifier = "0"; if (!amount) { throw new Error("Missing ERC20 amount for bulk transfer"); } transferAmount = amount.toString(); break; case types_1.TokenStandard.ERC721: itemType = 2; // ERC721 if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC721 tokenId for bulk transfer"); } identifier = asset.tokenId.toString(); transferAmount = "1"; break; case types_1.TokenStandard.ERC1155: itemType = 3; // ERC1155 if (asset.tokenId === undefined || asset.tokenId === null) { throw new Error("Missing ERC1155 tokenId for bulk transfer"); } if (!amount) { throw new Error("Missing ERC1155 amount for bulk transfer"); } identifier = asset.tokenId.toString(); transferAmount = amount.toString(); break; default: throw new Error(`Unsupported token standard for bulk transfer: ${asset.tokenStandard}`); } transferItems.push({ itemType, token: asset.tokenAddress, identifier, amount: transferAmount, recipient: toAddress, }); } // Check account availability after parameter validation await this.context.requireAccountIsAvailable(fromAddress); // Get the chain-specific default conduit const defaultConduit = (0, utils_1.getDefaultConduit)(this.context.chain); // Check approvals for all assets before attempting transfer const unapprovedAssets = []; for (const { asset, amount } of assets) { const isApproved = await this.checkAssetApproval(asset, fromAddress, defaultConduit.address, amount); if (!isApproved) { const assetIdentifier = asset.tokenId !== undefined ? `${asset.tokenAddress}:${asset.tokenId}` : asset.tokenAddress; unapprovedAssets.push(assetIdentifier); } } if (unapprovedAssets.length > 0) { throw new Error(`The following asset(s) are not approved for transfer to the OpenSea conduit:\n${unapprovedAssets.join("\n")}\n\n` + `Please approve these assets before transferring. You can use the batchApproveAssets() method to approve multiple assets efficiently in a single transaction.`); } // Create TransferHelper contract instance const transferHelper = this.getTransferHelperContract(); this.context.dispatch(types_1.EventType.Transfer, { accountAddress: fromAddress, assets, }); try { // Use chain-specific conduit key for bulk transfers const transaction = await transferHelper.bulkTransfer(transferItems, defaultConduit.key, { ...overrides, from: fromAddress }); await this.context.confirmTransaction(transaction.hash, types_1.EventType.Transfer, `Bulk transferring ${assets.length} asset(s)`); return transaction.hash; } catch (error) { console.error(error); this.context.dispatch(types_1.EventType.TransactionDenied, { error, accountAddress: fromAddress, }); throw error; } } /** * Batch approve multiple assets for transfer to the OpenSea conduit. * This method checks which assets need approval and batches them efficiently: * - 0 approvals needed: Returns early * - 1 approval needed: Sends single transaction * - 2+ approvals needed: Uses Multicall3 to batch all approvals in one transaction * * @param options * @param options.assets Array of assets to approve for transfer * @param options.fromAddress The address that owns the assets * @param options.overrides Transaction overrides, ignored if not set. * @returns Transaction hash of the approval transaction, or undefined if no approvals needed * * @throws Error if the fromAddress is not available through wallet or provider. */ async batchApproveAssets({ assets, fromAddress, overrides, }) { // Validate basic parameters before making any blockchain calls if (assets.length === 0) { return undefined; } // Validate ERC20 assets have amounts before making any blockchain calls for (const { asset, amount } of assets) { if (asset.tokenStandard === types_1.TokenStandard.ERC20 && !amount) { throw new Error(`Amount required for ERC20 approval: ${asset.tokenAddress}`); } } // Check account availability after parameter validation await this.context.requireAccountIsAvailable(fromAddress); // Get the chain-specific default conduit const defaultConduit = (0, utils_1.getDefaultConduit)(this.context.chain); // Check which assets need approval and build approval calldata const approvalsNeeded = []; const processedContracts = new Set(); for (const { asset, amount } of assets) { const isApproved = await this.checkAssetApproval(asset, fromAddress, defaultConduit.address, amount); if (!isApproved) { // For ERC721/ERC1155, only approve once per contract if (asset.tokenStandard === types_1.TokenStandard.ERC721 || asset.tokenStandard === types_1.TokenStandard.ERC1155) { if (processedContracts.has(asset.tokenAddress.toLowerCase())) { continue; } processedContracts.add(asset.tokenAddress.toLowerCase()); // setApprovalForAll(operator, true) const iface = new ethers_1.ethers.Interface([ "function setApprovalForAll(address operator, bool approved)", ]); const callData = iface.encodeFunctionData("setApprovalForAll", [ defaultConduit.address, true, ]); approvalsNeeded.push({ target: asset.tokenAddress, callData, }); } else if (asset.tokenStandard === types_1.TokenStandard.ERC20) { // approve(spender, amount) - use max uint256 for unlimited const iface = new ethers_1.ethers.Interface([ "function approve(address spender, uint256 amount) returns (bool)", ]); const callData = iface.encodeFunctionData("approve", [ defaultConduit.address, ethers_1.ethers.MaxUint256, // Approve max for convenience ]); approvalsNeeded.push({ target: asset.tokenAddress, callData, }); } } } // No approvals needed if (approvalsNeeded.length === 0) { return undefined; } // Single approval: send directly if (approvalsNeeded.length === 1) { const { target, callData } = approvalsNeeded[0]; const signer = this.context.signerOrProvider; const tx = await signer.sendTransaction({ to: target, data: callData, ...overrides, from: fromAddress, }); await this.context.confirmTransaction(tx.hash, types_1.EventType.ApproveAllAssets, "Approving asset for transfer"); return tx.hash; } // Multiple approvals: use Multicall3 const multicall3 = this.getMulticall3Contract(); const calls = approvalsNeeded.map(({ target, callData }) => ({ target, allowFailure: false, callData, })); try { const transaction = await multicall3.aggregate3(calls, { ...overrides, from: fromAddress, }); await this.context.confirmTransaction(transaction.hash, types_1.EventType.ApproveAllAssets, `Batch approving ${approvalsNeeded.length} asset(s) for transfer`); return transaction.hash; } catch (error) { console.error(error); this.context.dispatch(types_1.EventType.TransactionDenied, { error, accountAddress: fromAddress, }); throw error; } } /** * Check if an asset is approved for transfer to a specific operator (conduit). * @param asset The asset to check approval for * @param owner The owner address * @param operator The operator address (conduit) * @param amount Optional amount for ERC20 tokens * @returns True if approved, false otherwise */ async checkAssetApproval(asset, owner, operator, amount) { try { switch (asset.tokenStandard) { case types_1.TokenStandard.ERC20: { const contract = contracts_1.ERC20__factory.connect(asset.tokenAddress, this.context.provider); const allowance = await contract.allowance.staticCall(owner, operator); // Check if allowance is sufficient if (!amount) { return false; } return allowance >= BigInt(amount.toString()); } case types_1.TokenStandard.ERC721: { const contract = contracts_1.ERC721__factory.connect(asset.tokenAddress, this.context.provider); // Check isApprovedForAll first const isApprovedForAll = await contract.isApprovedForAll.staticCall(owner, operator); if (isApprovedForAll) { return true; } // Check individual token approval if (asset.tokenId !== undefined && asset.tokenId !== null) { const approved = await contract.getApproved.staticCall(asset.tokenId); return approved.toLowerCase() === operator.toLowerCase(); } return false; } case types_1.TokenStandard.ERC1155: { const contract = contracts_1.ERC1155__factory.connect(asset.tokenAddress, this.context.provider); return await contract.isApprovedForAll.staticCall(owner, operator); } default: return false; } } catch (error) { // If there's an error checking approval (e.g., contract doesn't exist), return false this.context.logger(`Error checking approval for ${asset.tokenAddress}: ${error}`); return false; } } /** * Get a TransferHelper contract instance. * @returns Contract instance for TransferHelper */ getTransferHelperContract() { return new ethers_1.Contract(constants_1.TRANSFER_HELPER_ADDRESS, [ "function bulkTransfer(tuple(uint8 itemType, address token, uint256 identifier, uint256 amount, address recipient)[] items, bytes32 conduitKey) external returns (bytes4)", ], this.context.signerOrProvider); } /** * Get a Multicall3 contract instance. * @returns Contract instance for Multicall3 */ getMulticall3Contract() { return new ethers_1.Contract(constants_1.MULTICALL3_ADDRESS, [ "function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)", ], this.context.signerOrProvider); } } exports.AssetsManager = AssetsManager; //# sourceMappingURL=assets.js.map