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
JavaScript
;
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