UPDATE 2022
Time passes, experience is added ;)
You can use signed messages. Have a look at this contract: https://etherscan.io/address/0xF2Bce1776A49250A5cad0F81aeb5C21Be72F537a#code
On line 374 (File 1 of 21):
function mintById_SignedMessage(uint256 _tokenId, uint256 _setPrice, uint256 expirationTimestamp, uint256 _nonce, bytes memory _sig) public payable{
// check validity and execute
require(expirationTimestamp <= block.timestamp, "Expired");
bytes32 message = SignedMessages.prefixed(keccak256(abi.encodePacked(msg.sender, _tokenId, _setPrice, expirationTimestamp, _nonce)));
require(msg.value == _setPrice, "Wrong payment");
require(SignedMessages.consumePass(message, _sig, _nonce), "Error in signed msg");
_internalMintById(msg.sender, _tokenId);
if (msg.value > 0) {
emit FundsReceived(msg.sender, msg.value, "payment by minting sale");
}
}
This function is called by passing so called "minting passes". Head down to file 3 of 21: This is the contract that checks the signature. So, the call parameters get signed with the private key of a valid "issuer" offchain (by you). Then you provide this signed message to the users (e.g. via frontend or backend) and when they call the contract, they can provide that signed message. Your contract can verify it has been issued by the right issuer, as shown here:
function consumePass(bytes32 message, bytes memory sig, uint256 nonce) internal returns(bool){
// check the nonce first
if (nonces[nonce]) {
return false;
}
// check the issuer
if (!issuers[recoverSigner(message, sig)]) {
return false;
}
// consume the nonce if it is safe
nonces[nonce] = true;
return true;
}
Some further links: