How to implement mint allowlist on Aptos

|
by: imcoding.online

In the code of the previous post, anyone can mint the NFT. Sometimes we need to verify whether the minter is in the allowlist, we can do this with the ed23319 asymmetric encryption.

You can skip the post and found the full code here.

Verify the minter

First, create ed23319 key pair via aptos command line tool.

$ aptos key generate --assume-yes --output-file key
$ cat key
C4E98CDCFF38D8DD9BD5F5456B489212ED2965BA8789CD70612EBE20C26E4873
$ cat key.pub
4092DAED9CCD916BB1E9814E5C2D0660262C4ED62F1750AF4B38110FC73D4D53

Then, we create a struct named MintProofChallenge stores the challenge message that proves the resource signer wants to mint this token to the receiver. And store the ed23319 public key in the NFTMinter resource. We also define an error code species the proof is invalid.

// file: imcoding_nft.move

// ...
use aptos_std::ed25519;
use std::error;
// ...

/// error code specifies the proof is invalid
const EINVALID_PROOF: u64 = 1;

struct MintProofChallenge has drop {
  receiver_account_sequence_number: u64,
  receiver_account_address: address,
}

struct NFTMinter has key {
  signer_cap: account::SignerCapability,
  collection: String,
  public_key: ed25519::ValidatedPublicKey,
}

And set the public key generated above in init_module function:

fun init_module(resource_account: &signer) {
  // ...

  let hardcoed_pk: vector<u8> = x"4092DAED9CCD916BB1E9814E5C2D0660262C4ED62F1750AF4B38110FC73D4D53";
  let public_key = std::option::extract(&mut ed25519::new_validated_public_key_from_bytes(hardcoed_pk));
  move_to(resource_account, NFTMinter {
    signer_cap: resource_signer_cap,
    collection: collection_name,
    public_key: public_key,
  });
}

In the mint_nft function, we add an input parameter named proof_signature and verify it with the public key in the body.

public entry fun mint_nft(receiver: &signer, proof_signature: vector<u8>) acquires NFTMinter {
  let nft_minter = borrow_global_mut<NFTMinter>(@imcoding_nft);
  let receiver_addr = address_of(receiver);
  verify_proof(receiver_addr, proof_signature, nft_minter.public_key);

  // ...
}

Let’s implement the verify_proof function:

fun verify_proof(receiver_addr: address, proof_signature: vector<u8>, public_key: ed25519::ValidatedPublicKey) {
  let sequence_number = account::get_sequence_number(receiver_addr);

  let proof_challenge = MintProofChallenge {
    receiver_account_sequence_number: sequence_number,
    receiver_account_address: receiver_addr,
  };

  let signature = ed25519::new_signature_from_bytes(proof_signature);
  let unvalidated_public_key = ed25519::public_key_to_unvalidated(&public_key);
  assert!(ed25519::signature_verify_strict_t(&signature, &unvalidated_public_key, proof_challenge), error::invalid_argument(EINVALID_PROOF));
}

Republish this module, next we will generate a proof signature to mint the NFT.

Generate proof signature

We generate proof signature in Typescript:

import { BCS, AptosAccount, TxnBuilderTypes, AptosClient } from "aptos";

class MintProofChallenge {
  moduleAddress: string;
  moduleName: string;
  structName: string;
  sequenceNumber: number;
  address: string;

  constructor(sequenceNumber: number, address: string) {
    this.moduleAddress = "e678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9";
    this.moduleName = "minting";
    this.structName = "MintProofChallenge";
    this.sequenceNumber = sequenceNumber;
    this.address = address;
  }

  serialize(serializer: BCS.Serializer) {
    TxnBuilderTypes.AccountAddress.fromHex(this.moduleAddress).serialize(serializer);
    serializer.serializeStr(this.moduleName);
    serializer.serializeStr(this.structName);
    serializer.serializeU64(this.sequenceNumber);
    TxnBuilderTypes.AccountAddress.fromHex(this.address).serialize(serializer);
  };
}

const generateProofSignature = async (address: string): string => {
  const claimKey = "C4E98CDCFF38D8DD9BD5F5456B489212ED2965BA8789CD70612EBE20C26E4873";
  const client = new AptosClient(process.env.APTOS_NODE_URL || "http://127.0.0.1:8080");
  const account = await client.getAccount(address);

  const proof = new MintProofChallenge(
    parseInt(account.sequence_number),
    address,
  );

  const proofMsg = BCS.bcsToBytes(proof);
  const signAccount = new AptosAccount(Uint8Array.from(Buffer.from(claimKey, "hex")));
  const signature = signAccount.signBuffer(proofMsg);
  return signature.noPrefix();
}


generateProofSignature("35a18f9201d2d6a9e3c86c4b9a00cb4444129cd2dc2fff72719240f8cb394016").then(console.log);

Run the code above, we can get the proof signature for account 35a18...94016

f094a82ec993a1dab09ce249d6e859afd3f2255103cfb3c47867af329bfd494980a7693dc104bc23b320dfa406e66818c160f50dfedf881bdd31e55fa86a9402

Invoke the mint_nft function with proof signature to mint the NFT.

aptos move run --function-id 0xe678abebea551c752030dfe6c78147e62b393b74163a4f167ab3444e0eda55a9::minting::mint_nft --args hex:f094a82ec993a1dab09ce249d6e859afd3f2255103cfb3c47867af329bfd494980a7693dc104bc23b320dfa406e66818c160f50dfedf881bdd31e55fa86a9402

If all goes well, we can mint success. The full code so far can be found here.