Step-by-Step Guide to Setting Up the Program and Writing the Constructor
Create a New Project Folder:
Initialize the Anchor Project with Solidity:
anchor init pda-mint-authority --solidity
in the terminal within your project folder.Navigate to the Project Directory:
cd pda-mint-authority
.Open the Project in Visual Studio Code (VSCode):
code .
to open your project in VSCode.Incorporate Essential Libraries:
Edit and Configure anchor.toml
File:
anchor.toml
file according to your project requirements.Importing Required Libraries:
pda-mint-authority.sol
) by importing necessary libraries from the root folder: import "../libraries/spl_token.sol";
import "solana";
Define the Program ID:
@program_id
annotation to specify your contract's on-chain address: @program_id("4Huxs8ujxKqT76qqfmfJXMThmo4WMsvmdiCkqzNKF5kq")
contract pda_mint_authority {
bytes1 bump; // stores the bump for the pda address
@payer(payer)
@seed("mint_authority") // hard-coded seed
constructor(
@bump bytes1 _bump // bump for the pda address
) {
(address pda, bytes1 pdaBump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
require(pdaBump == _bump, 'INVALID_BUMP');
bump = _bump;
}
}
Constructor Setup:
@payer
, @seed
, and @bump
annotations to define the contract's initialization logic.PDA Derivation and Verification:
try_find_program_address
to derive the PDA address and bump from the specified seed and program ID. constructor(@bump bytes1 _bump) {
(address pda, bytes1 pdaBump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
require(pdaBump == _bump, 'INVALID_BUMP');
bump = _bump;
}
try_find_program_address
Function:
Solidity require
Check:
require
statement validates that the bump used to generate the PDA is consistent each time the constructor is invoked, ensuring the uniqueness and security of the PDA.Role of @payer
and @seed
Annotations:
@payer
annotation specifies the account responsible for transaction fees and contract creation.@seed
annotation, particularly "mint_authority"
in this case, is used to derive the PDA and verify its authenticity.Adding the Function in Solidity:
createTokenMint
function to handle mint creation: @mutableSigner(payer)
@mutableSigner(mint)
@mutableAccount(metadata)
// Additional account annotations
function createTokenMint(/* parameters */) external {
// Function logic
}
Setting Up Accounts and Parameters:
@mutableSigner(payer)
: The account that pays for the transaction.@mutableSigner(mint)
: The account where the minted token will be created.@mutableAccount(metadata)
: The account for storing token metadata.Defining Function Arguments:
createTokenMint
accepts parameters for setting up the mint account:address freezeAuthority
: The authority that can freeze the mint account.uint8 decimals
: The decimal precision for the token.string name
, symbol
, uri
: Attributes for the metadata account.Invoke System and Token Programs:
SplToken.create_mint
function to establish the mint account and set attributes like mint authority, freeze authority, and decimals. SplToken.create_mint(
tx.accounts.payer.key, // Payer account
tx.accounts.mint.key, // Mint account
tx.accounts.mintAuthority.key, // Mint authority
freezeAuthority, // Freeze authority
decimals // Decimals
);
_createMetadataAccount
to create the metadata account: _createMetadataAccount(
tx.accounts.metadata.key, // Metadata account
tx.accounts.mint.key, // Mint account
tx.accounts.mintAuthority.key, // Mint authority
// Additional parameters
);
createTokenMint
function first sets up the mint account using the SPL token library, then creates the metadata account.The createTokenMint
function should look like this:
@mutableSigner(payer)
@mutableSigner(mint)
@mutableAccount(metadata)
@account(mintAuthority)
@account(rentAddress)
@account(metaplexId)
function createTokenMint(
address freezeAuthority,
uint8 decimals,
string name,
string symbol,
string uri
) external {
SplToken.create_mint(
tx.accounts.payer.key,
tx.accounts.mint.key,
tx.accounts.mintAuthority.key,
freezeAuthority,
decimals
);
_createMetadataAccount(
tx.accounts.metadata.key,
tx.accounts.mint.key,
tx.accounts.mintAuthority.key,
tx.accounts.payer.key,
tx.accounts.payer.key,
name,
symbol,
uri,
tx.accounts.rentAddress.key,
tx.accounts.metaplexId.key
);
}
@mutableSigner
and @mutableAccount
: Indicate accounts that will be mutated or changed during the transaction.@account
: Used for accounts that are part of the transaction but not necessarily mutated.Objective: To build a Solana SPL NFT minter program using Program Derived Addresses (PDAs) as mint authorities.
Approach: Implementing PDAs requires customizing certain library methods, particularly from the MPL (Metaplex) and SPL (Solana token program) library.
Defining the _createMetadataAccount
Method
Function Purpose: To create a metadata account for the minted token, signed with a PDA as the mint authority.
Method Declaration: The method is defined as a private function within the contract:
// Create metadata account, must reimplement manually to sign with PDA
function _createMetadataAccount(
address metadata,
address mint,
address mintAuthority,
address payer,
address updateAuthority,
string name,
string symbol,
string uri,
address rentAddress,
address metaplexId
) private {
// Implementation details
}
Arguments of the Method:
address metadata
: Address for the token's metadata account.address mint
: Address of the mint account for the token.address mintAuthority
: The mint authority, represented by a PDA.Deriving the PDA:
try_find_program_address
to derive the PDA from seeds and the program ID: (address pda, bytes1 _bump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
require(mintAuthority == pda, 'INVALID_PDA');
DataV2
and CreateMetadataAccountArgsV3
structs with the provided token attributes. DataV2 data = DataV2({
name: name,
symbol: symbol,
uri: uri,
sellerFeeBasisPoints: 0,
creatorsPresent: false,
collectionPresent: false,
usesPresent: false
});
CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
data: data,
isMutable: true,
collectionDetailsPresent: false
});
Account Metadata Configuration:
Configures AccountMeta
array with necessary accounts for the transaction:
Accounts include metadata, mint, mintAuthority, payer, updateAuthority, system program, and rent account.
AccountMeta[7] metas = [
AccountMeta({pubkey: metadata, is_writable: true, is_signer: false}),
AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),
AccountMeta({pubkey: mintAuthority, is_writable: false, is_signer: true}),
AccountMeta({pubkey: payer, is_writable: true, is_signer: true}),
AccountMeta({pubkey: updateAuthority, is_writable: false, is_signer: false}),
AccountMeta({pubkey: address"11111111111111111111111111111111", is_writable: false, is_signer: false}),
AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})
];
Encoding Instruction Data:
abi.encode
, including the discriminator and arguments. bytes1 discriminator = 33;
bytes instructionData = abi.encode(discriminator, args);
metaplexId.call{accounts: metas, seeds: [["mint_authority", abi.encode(_bump)]]}(instructionData);
try_find_program_address
Function: Generates a PDA from given seeds and the program ID.Formulating Solidity Structs and Metadata Account Creation Process
Creating Solidity Structs for NFT Metadata
Defining Structs:
DataV2
and CreateMetadataAccountArgsV3
, to organize NFT metadata and account creation arguments: struct CreateMetadataAccountArgsV3 {
DataV2 data;
bool isMutable;
bool collectionDetailsPresent;
}
struct DataV2 {
string name;
string symbol;
string uri;
uint16 sellerFeeBasisPoints;
bool creatorsPresent;
bool collectionPresent;
bool usesPresent;
}
Initializing Structs:
DataV2 data = DataV2({
name: name,
symbol: symbol,
uri: uri,
sellerFeeBasisPoints: 0,
creatorsPresent: false,
collectionPresent: false,
usesPresent: false
});
CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
data: data,
isMutable: true,
collectionDetailsPresent: false
});
Account Metadata for the Instruction
Configuring Account Metadata:
AccountMeta
array to define accounts involved in the transaction: AccountMeta[7] metas = [
AccountMeta({pubkey: metadata, is_writable: true, is_signer: false}),
// Additional AccountMeta configurations
];
Role of Mint Authority (PDA):
mintAuthority
account, represented by a PDA, plays a crucial role in signing the transaction.Encoding Instruction Data:
bytes1 discriminator = 33;
bytes instructionData = abi.encode(discriminator, args);
metaplexId.call{accounts: metas, seeds: [["mint_authority", abi.encode(_bump)]]}(instructionData);
DataV2
and CreateMetadataAccountArgsV3
: Used to structure and manage NFT metadata information.AccountMeta
) Configuration: Determines the roles and permissions of each account in the transaction.By integrating these elements, the Solidity Solang program effectively creates a mint account for NFTs and sets up a corresponding metadata account. The use of Program Derived Addresses (PDAs) as mint authorities adds an extra layer of security and control to the minting process. This approach aligns with Solana's programming model, ensuring the NFT minted is authenticated and managed correctly.