I am working on a slightly modified minter process in which the caller provides the process with token metadata (name, description, and the digital asset), and the process handles the entire minting process, with an option to then list the token for sale on a designated marketplace (OpenSea for now).
These are the smart contracts:
Factory
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "./ERC721Tradable.sol";
contract NFTContractFactory is Ownable
{
event ContractCreated(address addr);
function createContract(string memory name, string memory symbol, address proxyRegistryAddress)
public onlyOwner
{
NFTContract nc = new NFTContract(name, symbol, proxyRegistryAddress);
nc.transferOwnership(msg.sender);
emit ContractCreated(address(nc));
}
}
contract NFTContract is ERC721Tradable {
constructor(string memory _name, string memory _symbol, address _proxyRegistryAddress)
ERC721Tradable(_name, _symbol, _proxyRegistryAddress)
{}
// for now this just implements the interface, depending on needs can add in other methods
}
Contract
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "./common/meta-transactions/ContentMixin.sol";
import "./common/meta-transactions/NativeMetaTransaction.sol";
contract OwnableDelegateProxy {}
/**
* Used to delegate ownership of a contract to another address, to save on unneeded transactions to approve contract use for users
*/
contract ProxyRegistry {
mapping(address => OwnableDelegateProxy) public proxies;
}
/**
* @title ERC721Tradable
* ERC721Tradable - ERC721 contract that whitelists a trading address, and has minting functionality.
*/
abstract contract ERC721Tradable is ERC721, ContextMixin, NativeMetaTransaction, Ownable {
using SafeMath for uint256;
using Counters for Counters.Counter;
using Strings for uint256;
/**
* We rely on the OZ Counter util to keep track of the next available ID.
* We track the nextTokenId instead of the currentTokenId to save users on gas costs.
* Read more about it here: https://shiny.mirror.xyz/OUampBbIz9ebEicfGnQf5At_ReMHlZy0tB4glb9xQ0E
*/
Counters.Counter _nextTokenId;
address proxyRegistryAddress;
// Optional mapping for token URIs (taken from ERC721URIStorage)
mapping(uint256 => string) private _tokenURIs;
constructor(
string memory _name,
string memory _symbol,
address _proxyRegistryAddress
) ERC721(_name, _symbol) {
proxyRegistryAddress = _proxyRegistryAddress;
// nextTokenId is initialized to 1, since starting at 0 leads to higher gas cost for the first minter
_nextTokenId.increment();
_initializeEIP712(_name);
}
/**
* @dev Mints to an address a token with custom token URI and ID
* @param _to address of the future owner of the token
* @param turi custom token URI
* @param tokenId custom token ID
* Added by ck
*/
function mintTo(address _to, string memory turi, uint256 tokenId) public onlyOwner {
require(!_exists(tokenId), "Token ID already exists");
_nextTokenId.increment();
_safeMint(_to, tokenId);
_setTokenURI(tokenId, turi);
}
/**
@dev Returns the total tokens minted so far.
1 is always subtracted from the Counter since it tracks the next available tokenId.
*/
function totalSupply() public view returns (uint256) {
return _nextTokenId.current() - 1;
}
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token");
string memory _tokenURI = _tokenURIs[tokenId];
string memory base = _baseURI();
// If there is no base URI, return the token URI.
if (bytes(base).length == 0) {
return _tokenURI;
}
// If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked).
if (bytes(_tokenURI).length > 0) {
return string(abi.encodePacked(base, _tokenURI));
}
return super.tokenURI(tokenId);
}
/**
* @dev Sets `_tokenURI` as the tokenURI of `tokenId`.
*
* Requirements:
*
* - `tokenId` must exist.
* Added by ck; taken from ERC721URIStorage
*/
function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual {
require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token");
_tokenURIs[tokenId] = _tokenURI;
}
/**
* @dev Destroys `tokenId`.
* The approval is cleared when the token is burned.
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Transfer} event.
*/
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
if (bytes(_tokenURIs[tokenId]).length != 0) {
delete _tokenURIs[tokenId];
}
}
/**
* Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
*/
function isApprovedForAll(address owner, address operator) override public view returns (bool)
{
// Whitelist OpenSea proxy contract for easy trading.
ProxyRegistry proxyRegistry = ProxyRegistry(proxyRegistryAddress);
if (address(proxyRegistry.proxies(owner)) == operator) {
return true;
}
return super.isApprovedForAll(owner, operator);
}
/**
* This is used instead of msg.sender as transactions won't be sent by the original token owner, but by OpenSea.
*/
function _msgSender() override internal view returns (address sender)
{
return ContextMixin.msgSender();
}
}
This is the code to create the OpenSea client:
const opensea = require("opensea-js");
const OpenSeaPort = opensea.OpenSeaPort;
const PrivateKeyWalletSubprovider = require("@0x/subproviders").PrivateKeyWalletSubprovider;
const RPCSubprovider = require("web3-provider-engine/subproviders/rpc");
const Web3ProviderEngine = require("web3-provider-engine");
const config = require("./sellerConfig.js");
const openseaUtil = require("opensea-js/lib/utils/utils");
const providerEngine = new Web3ProviderEngine();
const walletSubprovider = new PrivateKeyWalletSubprovider(config.privateKey, config.chainId);
providerEngine.addProvider(walletSubprovider);
const rpcSubprovider = new RPCSubprovider({
rpcUrl: config.rpcEndpoint
});
providerEngine.addProvider(rpcSubprovider);
providerEngine.start();
const seaport = new OpenSeaPort(
providerEngine,
{
networkName: config.network,
apiKey: config.openseaAPIKey,
},
(arg) => console.log(arg)
);
module.exports.servicePort = config.servicePort;
module.exports.querySingleListing = config.querySingleListing;
module.exports.wethAddress = config.wethAddress;
module.exports.seaport = seaport;
module.exports.convertJsonToOrder = openseaUtil.orderFromJSON;
The value of config.privateKey
is the private key of the deployer account.
This is the code to generate a fixed-price sell order:
app.post('/create/fixedprice', async (req, res, next) => {
const { error } = validateFPSO(req.body);
if (error) {
let errMsg = error.details[0].message;
winston.error(`Failed to validate create fixed-price-order - ${errMsg}`);
res.status(400).send(errMsg);
return;
}
try {
const fixedPriceSellOrder = await seaport.createSellOrder({
asset: {
tokenId: req.body.tokenId,
tokenAddress: req.body.contractAddress,
},
startAmount: req.body.startAmount,
expirationTime: req.body.expirationTime,
accountAddress: req.body.ownerAddress,
});
winston.info(`Successfully created a fixed-price sell order! ${fixedPriceSellOrder.hash}\n`);
winston.debug(`Returning hash = ${fixedPriceSellOrder.hash} and opensea-link = ${fixedPriceSellOrder.asset.openseaLink}`);
res.json({
orderHash: fixedPriceSellOrder.hash,
listing_url: fixedPriceSellOrder.asset.openseaLink
});
} catch (error) {
winston.debug(`Error caught - ${error}`);
return next(error);
}
});
When I run this code, I get the following error:
Already approved proxy for this token
Error: Requested to sign message with address: <owner address>, instantiated with address: <deployer address>
at PrivateKeyWalletSubprovider.<anonymous> (/root/service/node_modules/@0x/subproviders/src/subproviders/private_key_wallet.ts:84:19)
at Generator.next (<anonymous>)
at /root/service/node_modules/@0x/subproviders/lib/src/subproviders/private_key_wallet.js:8:71
at new Promise (<anonymous>)
at __awaiter (/root/service/node_modules/@0x/subproviders/lib/src/subproviders/private_key_wallet.js:4:12)
at PrivateKeyWalletSubprovider.signPersonalMessageAsync (/root/service/node_modules/@0x/subproviders/lib/src/subproviders/private_key_wallet.js:94:16)
at PrivateKeyWalletSubprovider.<anonymous> (/root/service/node_modules/@0x/subproviders/src/subproviders/base_wallet_subprovider.ts:94:55)
at Generator.next (<anonymous>)
at /root/service/node_modules/@0x/subproviders/lib/src/subproviders/base_wallet_subprovider.js:8:71
at new Promise (<anonymous>)
at __awaiter (/root/service/node_modules/@0x/subproviders/lib/src/subproviders/base_wallet_subprovider.js:4:12)
at PrivateKeyWalletSubprovider.handleRequest (/root/service/node_modules/@0x/subproviders/lib/src/subproviders/base_wallet_subprovider.js:39:16)
at next (/root/service/node_modules/web3-provider-engine/index.js:116:18)
at Web3ProviderEngine._handleAsync (/root/service/node_modules/web3-provider-engine/index.js:103:3)
at Timeout._onTimeout (/root/service/node_modules/web3-provider-engine/index.js:87:12)
How should I grant the deployer permission to create sell orders on behalf of the owner?