-
-
Save Deathwing/b49c03c465c17312f8affc536b5ea64d to your computer and use it in GitHub Desktop.
Working ERC721Factory + CreateSellOffer for OpenSea
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: CC-BY-NC-4.0 | |
pragma solidity 0.8.15; | |
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | |
import "@openzeppelin/contracts/utils/Context.sol"; | |
import "@openzeppelin/contracts/utils/Strings.sol"; | |
contract OwnableDelegateProxy {} | |
contract ProxyRegistry { | |
mapping(address => OwnableDelegateProxy) public proxies; | |
} | |
interface IERC721Optimized { | |
function factoryMint(address to, uint64 amount) external; | |
} | |
interface IERC721Factory { | |
function name() external view returns (string memory); | |
function symbol() external view returns (string memory); | |
function numOptions() external view returns (uint256); | |
function canMint(uint256 _optionId) external view returns (bool); | |
function tokenURI(uint256 _optionId) external view returns (string memory); | |
function supportsFactoryInterface() external view returns (bool); | |
function mint(uint256 _optionId, address _toAddress) external; | |
} | |
contract ERC721Factory is Context, IERC721, IERC721Factory { | |
using Strings for uint256; | |
struct OptionConfig { | |
uint64[] mintAmount; | |
} | |
string private _name; | |
string private _symbol; | |
string private _baseURI; | |
OptionConfig private _optionConfig; | |
IERC721Optimized private _erc721Optimized; | |
address private _proxyRegistryAddress; | |
mapping(address => mapping(address => bool)) private _operatorApprovals; | |
constructor(string memory name_, string memory symbol_, string memory baseURI_, OptionConfig memory optionConfig_, address payable erc721OptimizedAddress_, address proxyRegistryAddress_) { | |
require(bytes(name_).length > 0, "ERC721Factory: name can't be empty"); | |
require(bytes(symbol_).length > 0, "ERC721Factory: symbol can't be empty"); | |
require(bytes(baseURI_).length > 0, "ERC721Factory: baseURI can't be empty"); | |
require(optionConfig_.mintAmount.length > 0, "ERC721Factory: optionConfig's mintAmount can't be empty"); | |
require(erc721OptimizedAddress_ != address(0), "ERC721Factory: erc721OptimizedAddress can't be null address"); | |
if (proxyRegistryAddress_ != address(0)) | |
ProxyRegistry(proxyRegistryAddress_).proxies(_msgSender()); | |
_name = name_; | |
_symbol = symbol_; | |
_baseURI = baseURI_; | |
_optionConfig = optionConfig_; | |
_erc721Optimized = IERC721Optimized(erc721OptimizedAddress_); | |
_proxyRegistryAddress = proxyRegistryAddress_; | |
_fireTransferEvents(address(0), owner()); | |
} | |
function supportsInterface(bytes4 interfaceId) external pure returns (bool) { | |
return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Factory).interfaceId || interfaceId == type(IERC165).interfaceId; | |
} | |
function name() external view returns (string memory) { | |
return _name; | |
} | |
function symbol() external view returns (string memory) { | |
return _symbol; | |
} | |
function baseURI() external view returns (string memory) { | |
return _baseURI; | |
} | |
function optionConfig() external view returns (OptionConfig memory) { | |
return _optionConfig; | |
} | |
function erc721OptimizedAddress() external view returns (address) { | |
return address(_erc721Optimized); | |
} | |
function proxyRegistryAddress() external view returns (address) { | |
return _proxyRegistryAddress; | |
} | |
function numOptions() external view returns (uint256) { | |
return _optionConfig.mintAmount.length; | |
} | |
function setBaseURI(string calldata baseURI_) external onlyOwner { | |
require(bytes(baseURI_).length > 0, "ERC721Factory: baseURI can't be empty"); | |
_baseURI = baseURI_; | |
} | |
function setOptionConfig(OptionConfig memory optionConfig_) external onlyOwner { | |
require(optionConfig_.mintAmount.length > 0, "ERC721Factory: optionConfig's mintAmount can't be empty"); | |
_fireTransferEvents(owner(), address(0)); | |
_optionConfig = optionConfig_; | |
_fireTransferEvents(address(0), owner()); | |
} | |
function setProxyRegistryAddress(address proxyRegistryAddress_) external onlyOwner { | |
if (proxyRegistryAddress_ != address(0)) | |
ProxyRegistry(proxyRegistryAddress_).proxies(_msgSender()); | |
_proxyRegistryAddress = proxyRegistryAddress_; | |
} | |
function contractURI() external view returns (string memory) { | |
return string(abi.encodePacked(_baseURI, "contract")); | |
} | |
function canMint(uint256 _optionId) external view returns (bool) { | |
return _canMint(_optionId); | |
} | |
function tokenURI(uint256 _optionId) external view returns (string memory) { | |
return string(abi.encodePacked(_baseURI, _optionId.toString())); | |
} | |
function supportsFactoryInterface() external pure returns (bool) { | |
return true; | |
} | |
function mint(uint256 _optionId, address _toAddress) external { | |
_mint(_optionId, _toAddress); | |
} | |
function transferOwnership(address newOwner) override public onlyOwner { | |
address _prevOwner = owner(); | |
super.transferOwnership(newOwner); | |
_fireTransferEvents(_prevOwner, newOwner); | |
} | |
function _canMint(uint256 _optionId) private view returns (bool) { | |
if (_optionId >= _optionConfig.mintAmount.length) | |
return false; | |
return true; | |
} | |
function _mint(uint256 _optionId, address _toAddress) private { | |
assert((_proxyRegistryAddress != address(0) && address(ProxyRegistry(_proxyRegistryAddress).proxies(owner())) == _msgSender()) || owner() == _msgSender()); | |
require(_canMint(_optionId), "ERC721Factory: can't mint"); | |
_erc721Optimized.factoryMint(_toAddress, _optionConfig.mintAmount[_optionId]); | |
} | |
function _fireTransferEvents(address _from, address _to) private { | |
for (uint256 i = 0; i < _optionConfig.mintAmount.length; i++) | |
emit Transfer(_from, _to, i); | |
} | |
/** | |
* Hack to get things to work automatically on OpenSea. | |
* Use IERC721 so the frontend doesn't have to worry about different method names. | |
*/ | |
function approve(address, uint256) external {} | |
function setApprovalForAll(address _operator, bool _approved) external { | |
_operatorApprovals[_msgSender()][_operator] = _approved; | |
} | |
function transferFrom(address, address _to, uint256 _tokenId) external { | |
_mint(_tokenId, _to); | |
} | |
function safeTransferFrom(address, address _to, uint256 _tokenId) external { | |
_mint(_tokenId, _to); | |
} | |
function safeTransferFrom(address, address _to, uint256 _tokenId, bytes calldata) external { | |
_mint(_tokenId, _to); | |
} | |
function isApprovedForAll(address _owner, address _operator) external view returns (bool) { | |
if (owner() == _owner && _owner == _operator) | |
return true; | |
if (owner() == _owner && (_proxyRegistryAddress != address(0) && address(ProxyRegistry(_proxyRegistryAddress).proxies(_owner)) == _operator)) | |
return true; | |
return _operatorApprovals[_owner][_operator]; | |
} | |
function balanceOf(address _owner) external view returns (uint256 _balance) { | |
if (owner() == _owner) | |
_balance = _optionConfig.mintAmount.length; | |
} | |
function getApproved(uint256) external view returns (address) { | |
return owner(); | |
} | |
function ownerOf(uint256) external view returns (address) { | |
return owner(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const HDWalletProvider = require("@truffle/hdwallet-provider"); | |
const { Contract, getDefaultProvider, utils } = require('ethers'); | |
const { Network, OpenSeaSDK } = require('opensea-js'); | |
const { exit } = require('process'); | |
const factory_abi = [{ | |
"inputs": [], | |
"name": "optionConfig", | |
"outputs": [ | |
{ | |
"components": [ | |
{ | |
"internalType": "uint64[]", | |
"name": "mintAmount", | |
"type": "uint64[]" | |
} | |
], | |
"internalType": "struct ERC721Factory.OptionConfig", | |
"name": "", | |
"type": "tuple" | |
} | |
], | |
"stateMutability": "view", | |
"type": "function" | |
}]; | |
const configs = { | |
[Network.Main]: { | |
factoryAddress: '<FACTORY_ADDRESS>', | |
provider: 'https://mainnet.infura.io/v3/<API_KEY>', | |
apiKey: '<API_KEY>', | |
}, | |
[Network.Rinkeby]: { | |
factoryAddress: '<FACTORY_ADDRESS>', | |
provider: 'https://rinkeby.infura.io/v3/<API_KEY>', | |
apiKey: '<API_KEY>' | |
} | |
}; | |
async function createFactorySellOrders(network, privateKey, ordersPerOptionId, pricePerTokenInETH) { | |
const config = configs[network]; | |
const contractProvider = getDefaultProvider(config.provider); | |
const factoryContract = new Contract(config.factoryAddress, factory_abi, contractProvider); | |
const openseaProvider = new HDWalletProvider({ | |
privateKeys: [privateKey], | |
provider: config.provider | |
}); | |
const openseaSdk = new OpenSeaSDK( | |
openseaProvider.engine, { | |
networkName: network, | |
apiKey: config.apiKey | |
}); | |
const accountAddress = openseaProvider.getAddress(); | |
async function createSellOrder(optionId, startAmount) { | |
const sellOrder = { | |
asset: { | |
tokenId: optionId.toString(), | |
tokenAddress: factoryContract.address, | |
}, | |
accountAddress: accountAddress, | |
startAmount: startAmount | |
}; | |
return await openseaSdk.createSellOrder(sellOrder); | |
} | |
const optionConfig = await factoryContract.optionConfig(); | |
const postedOffers = {}; | |
try { | |
for (var i = 0; i < ordersPerOptionId; i++) { | |
for (var j = 0; j < optionConfig.mintAmount.length; j++) { | |
const optionId = j; | |
const amount = optionConfig.mintAmount[optionId].toNumber(); | |
const startAmountString = (amount * pricePerTokenInETH).toString(); | |
await createSellOrder(optionId, startAmountString); | |
if (!postedOffers[optionId]) | |
postedOffers[optionId] = { amount, startAmountString, posts: 0 }; | |
postedOffers[optionId].posts++; | |
} | |
} | |
} catch (e) { | |
console.error(e); | |
} | |
for (const optionId in postedOffers) | |
console.log(`Created ${postedOffers[optionId].posts} offers for optionId ${optionId} (amount: ${postedOffers[optionId].amount}) for ${postedOffers[optionId].startAmountString} ETH each.`); | |
} | |
(async () => { | |
const network = process.argv[2]; | |
const privateKey = process.argv[3]; | |
const ordersPerOptionId = Number.parseInt(process.argv[4]); | |
const pricePerTokenInETH = Number.parseFloat(process.argv[5]); | |
if (!network || !privateKey) | |
throw new Error('Usage: \'node opensea.js (main|rinkery) (factory-assets-owner-private-key) (order-count) (price-eth)\', i.e.: \'node opensea.js rinkery 0x123456789 100 0.1\''); | |
try { | |
await createFactorySellOrders(network, privateKey, ordersPerOptionId, pricePerTokenInETH); | |
exit(0); | |
} catch (e) { | |
console.error(e); | |
exit(1); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment