In this post, we will further enhance mint allowlist security and add a personalized index to the token.
You can skip the post and found the full code here.
Limit the number of mints
We can restrict each off-chain allowlisted user to mint only one NFT. It is very simple, just add the off-chain user identifier to the proof challenge and record the token index.
First, add the user identifier to the MintProofChallenge
struct:
struct MintProofChallenge has drop {
receiver_account_sequence_number: u64,
receiver_account_address: address,
user_identifier: vector<u8>,
}
// ...
fun verify_proof(receiver_addr: address, user_identifier: vector<u8>, proof_signature: vector<u8>, public_key: ed25519::ValidatedPublicKey) {
// ...
let proof_challenge = MintProofChallenge {
receiver_account_sequence_number: sequence_number,
receiver_account_address: receiver_addr,
user_identifier,
};
// ...
}
Then add a table named mints
to record the token index of every off-chain user:
// ...
use aptos_std::table_with_length::{Self, TableWithLength};
struct NFTMinter has key {
signer_cap: account::SignerCapability,
collection: String,
public_key: ed25519::ValidatedPublicKey,
mints: TableWithLength<vector<u8>, u64>,
}
/// error code specifies already minted
const EALREADY_MINTED: u64 = 2;
// ...
fun init_module(resource_account: &signer) {
// ...
move_to(resource_account, NFTMinter {
signer_cap: resource_signer_cap,
collection: collection_name,
public_key: public_key,
// don't forgot to initialize the table
mints: table_with_length::new(),
});
}
At last, in the mint_nft
function, we check if the off-chain user minted and record the token index.
public entry fun mint_nft(receiver: &signer, user_identifier: vector<u8>, proof_signature: vector<u8>) acquires NFTMinter {
// ...
verify_proof(receiver_addr, user_identifier, proof_signature, nft_minter.public_key);
// check if minted
assert!(!table_with_length::contains(&nft_minter.mints, user_identifier), error::aborted(EALREADY_MINTED));
// record the token index
let index = table_with_length::length(&nft_minter.mints);
table_with_length::add(&mut nft_minter.mints, user_identifier, index);
// ...
}
Personalized token name
We can add the token index to the token name and token uri, so that the token mint from each user is different.
public entry fun mint_nft(receiver: &signer, user_identifier: vector<u8>, proof_signature: vector<u8>) acquires NFTMinter {
// ...
let token_name = string::utf8(b"IMCODING NFT");
string::append_utf8(&mut token_name, b": ");
string::append_utf8(&mut token_name, u64_to_bytes(index));
let token_description = string::utf8(b"");
let token_uri = string::utf8(b"https://imcoding.online/properity/");
string::append_utf8(&mut token_uri, u64_to_bytes(index));
string::append_utf8(&mut token_uri, b".json");
// ...
}
fun u64_to_bytes(i: u64): vector<u8> {
let v = vector::empty<u8>();
while (i >= 10) {
vector::push_back(&mut v, (48 + i % 10 as u8));
i = i / 10;
};
vector::push_back(&mut v, (48 + i as u8));
vector::reverse(&mut v);
v
}
Mint NFT
Modify proof signature generate code (assume that the off-chain user id is 1000001
):
// ...
class MintProofChallenge {
// ...
identifier: Uint8Array;
constructor(sequenceNumber: number, address: string, identifier: Uint8Array) {
// ...
this.identifier = identifier;
}
serialize(serializer: BCS.Serializer) {
// ...
serializer.serializeBytes(this.identifier);
}
}
const generateProofSignature = async (userId: string, address: string): Promise<string> => {
// ...
const identifier = Buffer.from(userId);
console.log("identitifer:", identifier.toString("hex"));
const proof = new MintProofChallenge(
parseInt(account.sequence_number),
address,
Uint8Array.from(identifier),
);
// ...
}
generateProofSignature("2333333", "35a18f9201d2d6a9e3c86c4b9a00cb4444129cd2dc2fff72719240f8cb394016").then(console.log);
Genarete the identifier and proof signature, got:
identitifer: 32333333333333
977cf82a1f86879a0be90e33d72ec68b9c787a47ccfdd979d4735c25faf3ff8a469f3f40bbdb09eb7cb21bfcff79c4401d779d3172cb52fa9996c11b2366810b
Let's republish the module and mint the nft:
aptos move run --function-id 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting::mint_nft --args hex:32333333333333 --args hex:977cf82a1f86879a0be90e33d72ec68b9c787a47ccfdd979d4735c25faf3ff8a469f3f40bbdb09eb7cb21bfcff79c4401d779d3172cb52fa9996c11b2366810b
After successful execution, the event data of 0x3::token::CreateTokenDataEvent
is roughly as follows:
{
"description": "",
"id": {
"collection": "IMCODING NFT Collection",
"creator": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
"name": "IMCODING NFT: 0"
},
"maximum": "1",
"mutability_config": {
"description": false,
"maximum": false,
"properties": true,
"royalty": false,
"uri": false
},
"name": "IMCODING NFT: 0",
"property_keys": [],
"property_types": [],
"property_values": [],
"royalty_payee_address": "0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9",
"royalty_points_denominator": "1",
"royalty_points_numerator": "0",
"uri": "https://imcoding.online/properity/0.json"
}
Each token has a unique token name and uri. If you use this off-chain user to mint NFT again, you will get the following error:
{
"Error": "Simulation failed with status: Move abort in 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting: EALREADY_MINTED(0x70002): error code specifies already minted"
}
So far, the tutorial of mint nft on aptos has been completed, and the full code can be found here.