50 min read
Overview
Ready to launch a token on Solana using your Solidity knowledge? This guide introduces the revolutionary approach of using Solidity and Solang to create SPL tokens on Solana.
What You Will Do
- Learn some basics about building with Solang
- Create a fungible SPL token program (equivalent to ERC20 and ERC721 token standards on Ethereum) using Solidity and Solang
- Run tests to make sure the program works as expected
- Deploy the program to Solana and mint your token
What You Will Need
- Nodejs installed
- npm or yarn installed
- Rust and Cargo latest version installed
- Solana CLI latest version installed
- Anchor CLI latest version installed
- TypeScript latest version installed
Dependency | Version |
---|---|
node.js | 18.16.1 |
anchor cli | 0.29 |
solana cli | 1.16.5 |
tsc | 5.0.2 |
To ensure you are ready to start, verify that you have installed Solana 1.16+ and Anchor 0.28+. You can do this by running the following commands:
solana --version
anchor --version
Now, let's get started!
Solang Basics
Solang is a Solidity compiler targeting Solana's blockchain. It allows developers familiar with Solidity to write smart contracts in a language they are comfortable with and then compile these contracts to run on the Solana network.
Solang aims to be compatible with Solidity, although not all features are supported due to differences between Solana and Ethereum. It works alongside tools like Anchor and Solana CLI, enabling a smooth workflow for Ethereum developers moving to Solana.
If you are interested in Solana and Solidity and want to learn more about Solang, check our comprehensive guide.
Terminology Differences between Ethereum and Solana
When using Solidity and Solang for Solana, it's important to understand key terminology differences as summarized below.
This table is just a quick overview; if you want to learn about Solana deeply, check our Solana Fundamentals Reference Guide.
Ethereum | Solana | Description |
---|---|---|
Address | Account | In Ethereum, an 'address' is a storage location. In Solana, 'accounts' can hold data and are more dynamic. As a side note, Solana accounts do not start with the 0x prefix. |
Smart Contract | Program | Ethereum's 'smart contract' is known as a 'program' in Solana. |
Mainnet, Testnet | Mainnet, Devnet, Testnet | In Solana, developers use Devnet for their testing purposes, while Testnet is for Solana core developers in general. |
Tokens (ERC20, ERC721, etc.) | Tokens (SPL tokens) | Solana supports SPL tokens, whereas Ethereum supports ERC20, ERC721, and other token standards. |
Dev Tooling (Hardhat, Foundry, etc.) | Dev Tooling (Solang, Anchor, etc.) | Development tools vary for Ethereum and Solana. |
Wallets (Metamask, etc.) | Wallets (Phantom, etc.) | Different wallets are used for Ethereum (like Metamask) and Solana (like Phantom). |
Stateful Contracts | Stateless Programs | Ethereum contracts are stateful, storing state within the contract. Solana programs are stateless, with states stored in accounts. |
20-byte Public Key Addresses | 32-byte Public Key Addresses | Solana account addresses use a 32-byte public key, compared to Ethereum's 20-byte public key. |
Set Up Your QuickNode Endpoint
To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us.
You can now pay for a QuickNode plan using USDC on Solana. As the first multi-chain provider to accept Solana payments, we're streamlining the process for developers — whether you're creating a new account or managing an existing one. Learn more about paying with Solana here.
See why over 50% of projects on Solana choose QuickNode and sign up for a free account here. We're going to use a Solana Devnet endpoint.
Copy the HTTP Provider link:
Upload Token Icon and Metadata to IPFS
In our pursuit of decentralization, it's vital to make our token icon and metadata accessible to the public, ensuring optimal display on block explorers, wallets, and exchanges. We advocate utilizing IPFS for pinning and serving data. To provide a comprehensive guide, we'll cover two methods — leveraging QuickNode's managed IPFS service and running a local IPFS node.
- QuickNode IPFS
- Local IPFS Node
To learn more about the price information of QuickNode IPFS, view our pricing plans here.
Visit the QuickNode Dashboard and access the IPFS tab on the left sidebar.
Navigate to the Files tab, either using the New button for Upload a file or by dragging your desired file. Start by uploading the token icon image and then the metadata JSON file.
After uploading, click the file name in the Files tab and click the copy IPFS URL button. The IPFS URL should have a similar format to the one below.
https://qn-shared.quicknode-ipfs.com/ipfs/QmS4UopJD2P843YWHJxxoqHdgK1YQfw2AF48fFJbSoCSr7
Now, let's compose our metadata. Solana adopts Metaplex's Fungible Token Standard, necessitating name, symbol, description, and image. Create a new metadata JSON file (e.g., token.json
), replace IPFS_URL_OF_IMAGE with the IPFS URL of the uploaded image, and save it. After saving, upload your JSON file to IPFS, similar to the previous step.
{
"name": "My Awesome Token",
"symbol": "MAT",
"description": "This token is awesome!",
"image": "IPFS_URL_OF_IMAGE"
}
After uploading, click the file name in the Files tab and copy the IPFS URL for later use during token minting. It must look something like the one below.
https://qn-shared.quicknode-ipfs.com/ipfs/QmYCwFK1KJWCXBE1aiRzUP1BZruaPW2g3PyPF25QJzDpP5
Now that our files are pinned on IPFS via QuickNode let's proceed to mint our token!
For those choosing to run a local IPFS node, the first step is to install IPFS and publish files. Download and install the IPFS CLI based on your operating system, follow the installation guide in IPFS docs, create a project directory, and navigate to it.
Initialize the IPFS repo in your terminal:
ipfs init
In another terminal window, start the IPFS daemon, acting as your local IPFS node:
ipfs daemon
Feel free to use your image or metadata, or use the provided example image.
Move your chosen image into the project directory (e.g., token-logo.png
). Return to the previous terminal window and publish the image to IPFS:
ipfs add token-logo.png
Upon successful upload, you'll receive output similar to this:
Save the generated hash, appending the https://ipfs.io/ipfs/ suffix. The IPFS URL should have a similar format to the one below.
https://ipfs.io/ipfs/QmS4UopJD2P843YWHJxxoqHdgK1YQfw2AF48fFJbSoCSr7
Now, let's compose our metadata. Solana follows Metaplex's Fungible Token Standard, requiring name, symbol, description, and image. Create a new metadata JSON file (e.g., token.json
).
{
"name": "My Awesome Token",
"symbol": "MAT",
"description": "This token is awesome!",
"image": "IPFS_URL_OF_IMAGE"
}
Move your metadata JSON into the project directory (e.g., token.json
). Publish the image to IPFS:
ipfs add token.json
Update IPFS_URL_OF_IMAGE with the complete URL of the uploaded image. Save and upload the file.
Add the https://ipfs.io/ipfs/ suffix to the hash of token.json
. The complete URL looks like the one below.
https://ipfs.io/ipfs/QmTFyKYDUgftB9Tu17zfmoAKLDX1HeZctq3MEyHhxDvaHm
This URL is essential for later use during token minting.
With our files now on IPFS, let's move forward to mint our token!
Set Up the Development Environment
Before diving in, ensure that you have all prerequisites that are listed here installed on your system.
Step 1: Initiate Your Project
Initiate a new Solang project for your SPL token using Anchor by running the command below in your project directory.
anchor init my-spl-token --solidity
This command sets up a new folder named my-spl-token with essential files. The --solidity flag indicates the use of Solang for compiling. Navigate to your new project and open it in your favorite code editor.
cd my-spl-token
Install the packages using either yarn or npm.
- yarn
- npm
yarn add @coral-xyz/anchor @solana/spl-token
npm install @coral-xyz/anchor @solana/spl-token
Step 2: Create a Wallet
To start interacting with Solana, you'll need a wallet. If you don't already have a Solana wallet, create one using the Solana CLI. This command generates a new wallet and saves the keypair file as id.json in your current directory:
solana-keygen new --no-bip39-passphrase -o ./id.json
Save your seed phrase safely. Also, keep your public key handy, as you will need it in the following sections.
Note: If you already have a Solana wallet and are aware of your JSON keypair file, you can skip this step. Just make sure to configure your project's configuration file to point to your existing wallet's JSON file. This step ensures you have the necessary credentials to interact with the Solana network to deploy and test your SPL token.
Step 3: Configuring the Wallet
Ensure your project is linked to your wallet. To do so, modify the [provider] section in the Anchor.toml
file in your project's root, as shown below.
If you don't create a new wallet in the previous step since you already have a Solana wallet, replace
./id.json
with the path of your JSON keypair file accordingly.
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.localnet]
my_spl_token = "7HtSCtT6cKH4iZQEWuTFrUqm8nLd4Nxb8foXKdBhEpzU"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet"
wallet = "./id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
Confirm if your wallet is configured correctly by running the code below. It should return your wallet's public address. Keep it handy since it will be needed to get some free SOL on Devnet.
solana address -k ./id.json
Step 4: Link Your Wallet
Link your Solana CLI to this wallet and the devnet.
To use your own QuickNode Solana Devnet endpoint, simply replace
devnet
with your endpoint's URL.
solana config set -u devnet -k ./id.json
Double-check your configuration, ensuring alignment with Anchor.toml
.
solana config get
Step 5: Airdrop SOL
To get some free SOL on devnet, use QuickNode Multi-Chain Faucet. Simply type your Solana wallet's public address and get your SOL. Also, you can use the command below.
solana airdrop 1
After getting free SOL, check your balance. This command returns the SOL balance on devnet since we configured devnet in Step 4.
solana balance
Create Your SPL Token using Solidity
For now, you set up your development environment and created your token's metadata. Now, let's focus on coding.
As the SPL Token program is written in Rust, not Solidity, some library files are needed to establish the communication between the Solidity code and the SPL Token program. So, let's add some SPL Token program-related library files to the project before jumping into the SPL token code.
Step 1: Create Library Files
Create a folder, libraries
, in your project directory. Then, create three library files (mpl_metadata, spl_token, and system_instruction) in the libraries
folder by running the commands below.
mkdir libraries
echo > libraries/mpl_metadata.sol
echo > libraries/spl_token.sol
echo > libraries/system_instruction.sol
For now, your project's folder structure should be similar to the one below.
├── Anchor.toml
├── app
├── id.json
├── libraries
├── migrations
├── node_modules
├── package.json
├── solidity
├── target
├── tests
├── tsconfig.json
└── yarn.lock
MPL Metadata
Open the mpl_metadata.sol
file in the libraries
folder and modify it as below.
import 'solana';
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/instruction/metadata.rs#L449
// Solidity does not support Rust Option<> type, so we need to handle it manually
// Requires creating a struct for each combination of Option<> types
// If bool for Option<> type is false, comment out the corresponding struct field otherwise instruction fails with "invalid account data"
// TODO: figure out better way to handle Option<> types
library MplMetadata {
address constant systemAddress = address"11111111111111111111111111111111";
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/instruction/metadata.rs#L31
struct CreateMetadataAccountArgsV3 {
DataV2 data;
bool isMutable;
bool collectionDetailsPresent; // To handle Rust Option<> in Solidity
// CollectionDetails collectionDetails;
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/state/data.rs#L22
struct DataV2 {
string name;
string symbol;
string uri;
uint16 sellerFeeBasisPoints;
bool creatorsPresent; // To handle Rust Option<> in Solidity
// Creator[] creators;
bool collectionPresent; // To handle Rust Option<> in Solidity
// Collection collection;
bool usesPresent; // To handle Rust Option<> in Solidity
// Uses uses;
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L10
struct Creator {
address creatorAddress;
bool verified;
uint8 share;
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L66
struct Collection {
bool verified;
address key;
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/token-metadata/program/src/state/collection.rs#L57
struct CollectionDetails {
CollectionDetailsType detailType;
uint64 size;
}
enum CollectionDetailsType {
V1
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L43
struct Uses {
UseMethod useMethod;
uint64 remaining;
uint64 total;
}
// Reference: https://github.com/metaplex-foundation/metaplex-program-library/blob/master/bubblegum/program/src/state/metaplex_adapter.rs#L35
enum UseMethod {
Burn,
Multiple,
Single
}
function create_metadata_account(
address metadata,
address mint,
address mintAuthority,
address payer,
address updateAuthority,
string name,
string symbol,
string uri,
address rentAddress,
address metadataProgramId
) public {
// // Example of how to add a Creator[] array to the DataV2 struct
// Creator[] memory creators = new Creator[](1);
// creators[0] = Creator({
// creatorAddress: payer,
// verified: false,
// share: 100
// });
DataV2 data = DataV2({
name: name,
symbol: symbol,
uri: uri,
sellerFeeBasisPoints: 0,
creatorsPresent: false,
// creators: creators,
collectionPresent: false,
// collection: Collection({
// verified: false,
// key: address(0)
// }),
usesPresent: false
// uses: Uses({
// useMethod: UseMethod.Burn,
// remaining: 0,
// total: 0
// })
});
CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
data: data,
isMutable: true,
collectionDetailsPresent: false
// collectionDetails: CollectionDetails({
// detailType: CollectionDetailsType.V1,
// size: 0
// })
});
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: systemAddress, is_writable: false, is_signer: false}),
AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})
];
bytes1 discriminator = 33;
bytes instructionData = abi.encode(discriminator, args);
metadataProgramId.call{accounts: metas}(instructionData);
}
}
SPL Token
Open the spl_token.sol
file in the libraries
folder and modify it as below.
import 'solana';
import './system_instruction.sol';
library SplToken {
address constant tokenProgramId = address"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
address constant associatedTokenProgramId = address"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
address constant rentAddress = address"SysvarRent111111111111111111111111111111111";
enum TokenInstruction {
InitializeMint, // 0
InitializeAccount, // 1
InitializeMultisig, // 2
Transfer, // 3
Approve, // 4
Revoke, // 5
SetAuthority, // 6
MintTo, // 7
Burn, // 8
CloseAccount, // 9
FreezeAccount, // 10
ThawAccount, // 11
TransferChecked, // 12
ApproveChecked, // 13
MintToChecked, // 14
BurnChecked, // 15
InitializeAccount2, // 16
SyncNative, // 17
InitializeAccount3, // 18
InitializeMultisig2, // 19
InitializeMint2, // 20
GetAccountDataSize, // 21
InitializeImmutableOwner, // 22
AmountToUiAmount, // 23
UiAmountToAmount, // 24
InitializeMintCloseAuthority, // 25
TransferFeeExtension, // 26
ConfidentialTransferExtension, // 27
DefaultAccountStateExtension, // 28
Reallocate, // 29
MemoTransferExtension, // 30
CreateNativeMint // 31
}
/// Initialize a new token account.
///
/// @param tokenAccount the public key of the token account to initialize
/// @param mint the public key of the mint account for this new token account
/// @param owner the public key of the owner of this new token account
function initialize_account(address tokenAccount, address mint, address owner) internal{
bytes instr = new bytes(1);
instr[0] = uint8(TokenInstruction.InitializeAccount);
AccountMeta[4] metas = [
AccountMeta({pubkey: tokenAccount, is_writable: true, is_signer: false}),
AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: false, is_signer: false}),
AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Initialize a new associated token account.
///
/// @param payer the public key of the payer to create the associated token account
/// @param tokenAccount the public key of the token account to initialize
/// @param mint the public key of the mint account for this new token account
/// @param owner the public key of the owner of this new token account
function create_associated_token_account(address payer, address tokenAccount, address mint, address owner) internal {
AccountMeta[6] metas = [
AccountMeta({pubkey: payer, is_writable: true, is_signer: true}),
AccountMeta({pubkey: tokenAccount, is_writable: true, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: false, is_signer: false}),
AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),
AccountMeta({pubkey: SystemInstruction.systemAddress, is_writable: false, is_signer: false}),
AccountMeta({pubkey: SplToken.tokenProgramId, is_writable: false, is_signer: false})
];
bytes instructionData = abi.encode((0));
associatedTokenProgramId.call{accounts: metas}(instructionData);
}
// Initialize mint instruction data
struct InitializeMintInstruction {
uint8 instruction;
uint8 decimals;
address mintAuthority;
uint8 freezeAuthorityOption;
address freezeAuthority;
}
/// Initialize a new mint account.
///
/// @param mint the public key of the mint account to initialize
/// @param mintAuthority the public key of the mint authority
/// @param freezeAuthority the public key of the freeze authority
/// @param decimals the decimals of the mint
function initialize_mint(address mint, address mintAuthority, address freezeAuthority, uint8 decimals) internal {
InitializeMintInstruction instr = InitializeMintInstruction({
instruction: 20,
decimals: decimals,
mintAuthority: mintAuthority,
freezeAuthorityOption: 1,
freezeAuthority: freezeAuthority
});
AccountMeta[1] metas = [
AccountMeta({pubkey: mint, is_writable: true, is_signer: false})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Create and initialize a new mint account in one instruction
///
/// @param payer the public key of the account paying to create the mint account
/// @param mint the public key of the mint account to initialize
/// @param mintAuthority the public key of the mint authority
/// @param freezeAuthority the public key of the freeze authority
/// @param decimals the decimals of the mint
function create_mint(address payer, address mint, address mintAuthority, address freezeAuthority, uint8 decimals) internal {
// Invoke System Program to create a new account for the mint account
// Program owner is set to the Token program
SystemInstruction.create_account(
payer, // lamports sent from this account (payer)
mint, // lamports sent to this account (account to be created)
1461600, // lamport amount (minimum lamports for mint account)
82, // space required for the account (mint account)
SplToken.tokenProgramId // new program owner
);
InitializeMintInstruction instr = InitializeMintInstruction({
instruction: 20,
decimals: decimals,
mintAuthority: mintAuthority,
freezeAuthorityOption: 1,
freezeAuthority: freezeAuthority
});
AccountMeta[1] metas = [
AccountMeta({pubkey: mint, is_writable: true, is_signer: false})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Mint new tokens. The transaction should be signed by the mint authority keypair
///
/// @param mint the account of the mint
/// @param account the token account where the minted tokens should go
/// @param authority the public key of the mint authority
/// @param amount the amount of tokens to mint
function mint_to(address mint, address account, address authority, uint64 amount) internal {
bytes instr = new bytes(9);
instr[0] = uint8(TokenInstruction.MintTo);
instr.writeUint64LE(amount, 1);
AccountMeta[3] metas = [
AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),
AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
AccountMeta({pubkey: authority, is_writable: true, is_signer: true})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Transfer @amount token from @from to @to. The transaction should be signed by the owner
/// keypair of the from account.
///
/// @param from the account to transfer tokens from
/// @param to the account to transfer tokens to
/// @param owner the publickey of the from account owner keypair
/// @param amount the amount to transfer
function transfer(address from, address to, address owner, uint64 amount) internal {
bytes instr = new bytes(9);
instr[0] = uint8(TokenInstruction.Transfer);
instr.writeUint64LE(amount, 1);
AccountMeta[3] metas = [
AccountMeta({pubkey: from, is_writable: true, is_signer: false}),
AccountMeta({pubkey: to, is_writable: true, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: true, is_signer: true})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Burn @amount tokens in account. This transaction should be signed by the owner.
///
/// @param account the acount for which tokens should be burned
/// @param mint the mint for this token
/// @param owner the publickey of the account owner keypair
/// @param amount the amount to transfer
function burn(address account, address mint, address owner, uint64 amount) internal {
bytes instr = new bytes(9);
instr[0] = uint8(TokenInstruction.Burn);
instr.writeUint64LE(amount, 1);
AccountMeta[3] metas = [
AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
AccountMeta({pubkey: mint, is_writable: true, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: true, is_signer: true})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Approve an amount to a delegate. This transaction should be signed by the owner
///
/// @param account the account for which a delegate should be approved
/// @param delegate the delegate publickey
/// @param owner the publickey of the account owner keypair
/// @param amount the amount to approve
function approve(address account, address delegate, address owner, uint64 amount) internal {
bytes instr = new bytes(9);
instr[0] = uint8(TokenInstruction.Approve);
instr.writeUint64LE(amount, 1);
AccountMeta[3] metas = [
AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
AccountMeta({pubkey: delegate, is_writable: false, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Revoke a previously approved delegate. This transaction should be signed by the owner. After
/// this transaction, no delgate is approved for any amount.
///
/// @param account the account for which a delegate should be approved
/// @param owner the publickey of the account owner keypair
function revoke(address account, address owner) internal {
bytes instr = new bytes(1);
instr[0] = uint8(TokenInstruction.Revoke);
AccountMeta[2] metas = [
AccountMeta({pubkey: account, is_writable: true, is_signer: false}),
AccountMeta({pubkey: owner, is_writable: false, is_signer: true})
];
tokenProgramId.call{accounts: metas}(instr);
}
/// Get the total supply for the mint, i.e. the total amount in circulation
/// @param mint the mint for this token
function total_supply(address mint) internal view returns (uint64) {
AccountInfo account = get_account_info(mint);
return account.data.readUint64LE(36);
}
/// Get the balance for an account.
///
/// @param account the account for which we want to know a balance
function get_balance(address account) internal view returns (uint64) {
AccountInfo ai = get_account_info(account);
return ai.data.readUint64LE(64);
}
/// Get the account info for an account. This walks the transaction account infos
/// and find the account info, or the transaction fails.
///
/// @param account the account for which we want to have the acount info.
function get_account_info(address account) internal view returns (AccountInfo) {
for (uint64 i = 0; i < tx.accounts.length; i++) {
AccountInfo ai = tx.accounts[i];
if (ai.key == account) {
return ai;
}
}
revert("account missing");
}
/// This enum represents the state of a token account
enum AccountState {
Uninitialized,
Initialized,
Frozen
}
/// This struct is the return of 'get_token_account_data'
struct TokenAccountData {
address mintAccount;
address owner;
uint64 balance;
bool delegate_present;
address delegate;
AccountState state;
bool is_native_present;
uint64 is_native;
uint64 delegated_amount;
bool close_authority_present;
address close_authority;
}
/// Fetch the owner, mint account and balance for an associated token account.
///
/// @param tokenAccount The token account
/// @return struct TokenAccountData
function get_token_account_data(address tokenAccount) public view returns (TokenAccountData) {
AccountInfo ai = get_account_info(tokenAccount);
TokenAccountData data = TokenAccountData(
{
mintAccount: ai.data.readAddress(0),
owner: ai.data.readAddress(32),
balance: ai.data.readUint64LE(64),
delegate_present: ai.data.readUint32LE(72) > 0,
delegate: ai.data.readAddress(76),
state: AccountState(ai.data[108]),
is_native_present: ai.data.readUint32LE(109) > 0,
is_native: ai.data.readUint64LE(113),
delegated_amount: ai.data.readUint64LE(121),
close_authority_present: ai.data.readUint32LE(129) > 10,
close_authority: ai.data.readAddress(133)
}
);
return data;
}
// This struct is the return of 'get_mint_account_data'
struct MintAccountData {
bool authority_present;
address mint_authority;
uint64 supply;
uint8 decimals;
bool is_initialized;
bool freeze_authority_present;
address freeze_authority;
}
/// Retrieve the information saved in a mint account
///
/// @param mintAccount the account whose information we want to retrive
/// @return the MintAccountData struct
function get_mint_account_data(address mintAccount) public view returns (MintAccountData) {
AccountInfo ai = get_account_info(mintAccount);
uint32 authority_present = ai.data.readUint32LE(0);
uint32 freeze_authority_present = ai.data.readUint32LE(46);
MintAccountData data = MintAccountData( {
authority_present: authority_present > 0,
mint_authority: ai.data.readAddress(4),
supply: ai.data.readUint64LE(36),
decimals: uint8(ai.data[44]),
is_initialized: ai.data[45] > 0,
freeze_authority_present: freeze_authority_present > 0,
freeze_authority: ai.data.readAddress(50)
});
return data;
}
// A mint account has an authority, whose type is one of the members of this struct.
enum AuthorityType {
MintTokens,
FreezeAccount,
AccountOwner,
CloseAccount
}
/// Remove the mint authority from a mint account
///
/// @param mintAccount the public key for the mint account
/// @param mintAuthority the public for the mint authority
function remove_mint_authority(address mintAccount, address mintAuthority) public {
AccountMeta[2] metas = [
AccountMeta({pubkey: mintAccount, is_signer: false, is_writable: true}),
AccountMeta({pubkey: mintAuthority, is_signer: true, is_writable: false})
];
bytes data = new bytes(9);
data[0] = uint8(TokenInstruction.SetAuthority);
data[1] = uint8(AuthorityType.MintTokens);
data[3] = 0;
tokenProgramId.call{accounts: metas}(data);
}
}
System Instruction
Open the system_instruction.sol
file in the libraries
folder and modify it as below.
// SPDX-License-Identifier: Apache-2.0
// Disclaimer: This library provides a bridge for Solidity to interact with Solana's system instructions. Although it is production ready,
// it has not been audited for security, so use it at your own risk.
import 'solana';
library SystemInstruction {
address constant systemAddress = address"11111111111111111111111111111111";
address constant recentBlockHashes = address"SysvarRecentB1ockHashes11111111111111111111";
address constant rentAddress = address"SysvarRent111111111111111111111111111111111";
uint64 constant state_size = 80;
enum Instruction {
CreateAccount,
Assign,
Transfer,
CreateAccountWithSeed,
AdvanceNounceAccount,
WithdrawNonceAccount,
InitializeNonceAccount,
AuthorizeNonceAccount,
Allocate,
AllocateWithSeed,
AssignWithSeed,
TransferWithSeed,
UpgradeNonceAccount // This is not available on Solana v1.9.15
}
/// Create a new account on Solana
///
/// @param from public key for the account from which to transfer lamports to the new account
/// @param to public key for the account to be created
/// @param lamports amount of lamports to be transfered to the new account
/// @param space the size in bytes that is going to be made available for the account
/// @param owner public key for the program that will own the account being created
function create_account(address from, address to, uint64 lamports, uint64 space, address owner) internal {
AccountMeta[2] metas = [
AccountMeta({pubkey: from, is_signer: true, is_writable: true}),
AccountMeta({pubkey: to, is_signer: true, is_writable: true})
];
bytes bincode = abi.encode(uint32(Instruction.CreateAccount), lamports, space, owner);
systemAddress.call{accounts: metas}(bincode);
}
/// Create a new account on Solana using a public key derived from a seed
///
/// @param from public key for the account from which to transfer lamports to the new account
/// @param to the public key for the account to be created. The public key must match create_with_seed(base, seed, owner)
/// @param base the base address that derived the 'to' address using the seed
/// @param seed the string utilized to created the 'to' public key
/// @param lamports amount of lamports to be transfered to the new account
/// @param space the size in bytes that is going to be made available for the account
/// @param owner public key for the program that will own the account being created
function create_account_with_seed(address from, address to, address base, string seed, uint64 lamports, uint64 space, address owner) internal {
AccountMeta[3] metas = [
AccountMeta({pubkey: from, is_signer: true, is_writable: true}),
AccountMeta({pubkey: to, is_signer: false, is_writable: true}),
AccountMeta({pubkey: base, is_signer: true, is_writable: false})
];
uint32 buffer_size = 92 + seed.length;
bytes bincode = new bytes(buffer_size);
bincode.writeUint32LE(uint32(Instruction.CreateAccountWithSeed), 0);
bincode.writeAddress(base, 4);
bincode.writeUint64LE(uint64(seed.length), 36);
bincode.writeString(seed, 44);
uint32 offset = seed.length + 44;
bincode.writeUint64LE(lamports, offset);
offset += 8;
bincode.writeUint64LE(space, offset);
offset += 8;
bincode.writeAddress(owner, offset);
systemAddress.call{accounts: metas}(bincode);
}
/// Assign account to a program (owner)
///
/// @param pubkey the public key for the account whose owner is going to be reassigned
/// @param owner the public key for the new account owner
function assign(address pubkey, address owner) internal {
AccountMeta[1] meta = [
AccountMeta({pubkey: pubkey, is_signer: true, is_writable: true})
];
bytes bincode = abi.encode(uint32(Instruction.Assign), owner);
systemAddress.call{accounts: meta}(bincode);
}
/// Assign account to a program (owner) based on a seed
///
/// @param addr the public key for the account whose owner is going to be reassigned. The public key must match create_with_seed(base, seed, owner)
/// @param base the base address that derived the 'addr' key using the seed
/// @param seed the string utilized to created the 'addr' public key
/// @param owner the public key for the new program owner
function assign_with_seed(address addr, address base, string seed, address owner) internal {
AccountMeta[2] metas = [
AccountMeta({pubkey: addr, is_signer: false, is_writable: true}),
AccountMeta({pubkey: base, is_signer: true, is_writable: false})
];
uint32 buffer_size = 76 + seed.length;
bytes bincode = new bytes(buffer_size);
bincode.writeUint32LE(uint32(Instruction.AssignWithSeed), 0);
bincode.writeAddress(base, 4);
bincode.writeUint64LE(uint64(seed.length), 36);
bincode.writeString(seed, 44);
bincode.writeAddress(owner, 44 + seed.length);
systemAddress.call{accounts: metas}(bincode);
}
/// Transfer lamports between accounts
///
/// @param from public key for the funding account
/// @param to public key for the recipient account
/// @param lamports amount of lamports to transfer
function transfer(address from, address to, uint64 lamports) internal {
AccountMeta[2] metas = [
AccountMeta({pubkey: from, is_signer: true, is_writable: true}),
AccountMeta({pubkey: to, is_signer: false, is_writable: true})
];
bytes bincode = abi.encode(uint32(Instruction.Transfer), lamports);
systemAddress.call{accounts: metas}(bincode);
}
/// Transfer lamports from a derived address
///
/// @param from_pubkey The funding account public key. It should match create_with_seed(from_base, seed, from_owner)
/// @param from_base the base address that derived the 'from_pubkey' key using the seed
/// @param seed the string utilized to create the 'from_pubkey' public key
/// @param from_owner owner to use to derive the funding account address
/// @param to_pubkey the public key for the recipient account
/// @param lamports amount of lamports to transfer
function transfer_with_seed(address from_pubkey, address from_base, string seed, address from_owner, address to_pubkey, uint64 lamports) internal {
AccountMeta[3] metas = [
AccountMeta({pubkey: from_pubkey, is_signer: false, is_writable: true}),
AccountMeta({pubkey: from_base, is_signer: true, is_writable: false}),
AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true})
];
uint32 buffer_size = seed.length + 52;
bytes bincode = new bytes(buffer_size);
bincode.writeUint32LE(uint32(Instruction.TransferWithSeed), 0);
bincode.writeUint64LE(lamports, 4);
bincode.writeUint64LE(seed.length, 12);
bincode.writeString(seed, 20);
bincode.writeAddress(from_owner, 20 + seed.length);
systemAddress.call{accounts: metas}(bincode);
}
/// Allocate space in a (possibly new) account without funding
///
/// @param pub_key account for which to allocate space
/// @param space number of bytes of memory to allocate
function allocate(address pub_key, uint64 space) internal {
AccountMeta[1] meta = [
AccountMeta({pubkey: pub_key, is_signer: true, is_writable: true})
];
bytes bincode = abi.encode(uint32(Instruction.Allocate), space);
systemAddress.call{accounts: meta}(bincode);
}
/// Allocate space for an assign an account at an address derived from a base public key and a seed
///
/// @param addr account for which to allocate space. It should match create_with_seed(base, seed, owner)
/// @param base the base address that derived the 'addr' key using the seed
/// @param seed the string utilized to create the 'addr' public key
/// @param space number of bytes of memory to allocate
/// @param owner owner to use to derive the 'addr' account address
function allocate_with_seed(address addr, address base, string seed, uint64 space, address owner) internal {
AccountMeta[2] metas = [
AccountMeta({pubkey: addr, is_signer: false, is_writable: true}),
AccountMeta({pubkey: base, is_signer: true, is_writable: false})
];
bytes bincode = new bytes(seed.length + 84);
bincode.writeUint32LE(uint32(Instruction.AllocateWithSeed), 0);
bincode.writeAddress(base, 4);
bincode.writeUint64LE(seed.length, 36);
bincode.writeString(seed, 44);
uint32 offset = 44 + seed.length;
bincode.writeUint64LE(space, offset);
offset += 8;
bincode.writeAddress(owner, offset);
systemAddress.call{accounts: metas}(bincode);
}
/// Create a new nonce account on Solana using a public key derived from a seed
///
/// @param from public key for the account from which to transfer lamports to the new account
/// @param nonce the public key for the account to be created. The public key must match create_with_seed(base, seed, systemAddress)
/// @param base the base address that derived the 'nonce' key using the seed
/// @param seed the string utilized to create the 'addr' public key
/// @param authority The entity authorized to execute nonce instructions on the account
/// @param lamports amount of lamports to be transfered to the new account
function create_nonce_account_with_seed(address from, address nonce, address base, string seed, address authority, uint64 lamports) internal {
create_account_with_seed(from, nonce, base, seed, lamports, state_size, systemAddress);
AccountMeta[3] metas = [
AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}),
AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),
AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false})
];
bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority);
systemAddress.call{accounts: metas}(bincode);
}
/// Create a new account on Solana
///
/// @param from public key for the account from which to transfer lamports to the new account
/// @param nonce the public key for the nonce account to be created
/// @param authority The entity authorized to execute nonce instructions on the account
/// @param lamports amount of lamports to be transfered to the new account
function create_nonce_account(address from, address nonce, address authority, uint64 lamports) internal {
create_account(from, nonce, lamports, state_size, systemAddress);
AccountMeta[3] metas = [
AccountMeta({pubkey: nonce, is_signer: false, is_writable: true}),
AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),
AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false})
];
bytes bincode = abi.encode(uint32(Instruction.InitializeNonceAccount), authority);
systemAddress.call{accounts: metas}(bincode);
}
/// Consumes a stored nonce, replacing it with a successor
///
/// @param nonce_pubkey the public key for the nonce account
/// @param authorized_pubkey the publick key for the entity authorized to execute instructins on the account
function advance_nonce_account(address nonce_pubkey, address authorized_pubkey) internal {
AccountMeta[3] metas = [
AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),
AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),
AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})
];
bytes bincode = abi.encode(uint32(Instruction.AdvanceNounceAccount));
systemAddress.call{accounts: metas}(bincode);
}
/// Withdraw funds from a nonce account
///
/// @param nonce_pubkey the public key for the nonce account
/// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account
/// @param to_pubkey the recipient account
/// @param lamports the number of lamports to withdraw
function withdraw_nonce_account(address nonce_pubkey, address authorized_pubkey, address to_pubkey, uint64 lamports) internal {
AccountMeta[5] metas = [
AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),
AccountMeta({pubkey: to_pubkey, is_signer: false, is_writable: true}),
AccountMeta({pubkey: recentBlockHashes, is_signer: false, is_writable: false}),
AccountMeta({pubkey: rentAddress, is_signer: false, is_writable: false}),
AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})
];
bytes bincode = abi.encode(uint32(Instruction.WithdrawNonceAccount), lamports);
systemAddress.call{accounts: metas}(bincode);
}
/// Change the entity authorized to execute nonce instructions on the account
///
/// @param nonce_pubkey the public key for the nonce account
/// @param authorized_pubkey the public key for the entity authorized to execute instructins on the account
/// @param new_authority
function authorize_nonce_account(address nonce_pubkey, address authorized_pubkey, address new_authority) internal {
AccountMeta[2] metas = [
AccountMeta({pubkey: nonce_pubkey, is_signer: false, is_writable: true}),
AccountMeta({pubkey: authorized_pubkey, is_signer: true, is_writable: false})
];
bytes bincode = abi.encode(uint32(Instruction.AuthorizeNonceAccount), new_authority);
systemAddress.call{accounts: metas}(bincode);
}
/// One-time idempotent upgrade of legacy nonce version in order to bump them out of chain domain.
///
/// @param nonce the public key for the nonce account
// This is not available on Solana v1.9.15
function upgrade_nonce_account(address nonce) internal {
AccountMeta[1] meta = [
AccountMeta({pubkey: nonce, is_signer: false, is_writable: true})
];
bytes bincode = abi.encode(uint32(Instruction.UpgradeNonceAccount));
systemAddress.call{accounts: meta}(bincode);
}
}
Step 2: Create the SPL Token File
As the solidity
folder has been created automatically during the project initialization with a file, my-spl-token.sol
, you do not need to create any folder or file in this step.
The SPL Token Minter contract serves as a bridge between Solidity smart contract codes and the SPL token program on Solana. The contract includes functions to create a new token mint, specifying parameters like freeze authority, decimals, name, symbol, and URI for metadata. Additionally, it provides functionality to mint a specified amount of tokens to a designated token account within the created mint.
Open the my-spl-token.sol
file in the solidity
folder and modify it as below.
If you choose a different project name while initializing your project, your file name may differ accordingly.
// Import necessary libraries for SPL token and metadata handling.
import "../libraries/spl_token.sol";
import "../libraries/mpl_metadata.sol";
// Define the program contract with the specified program ID on the Solana blockchain.
@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract spl_token_minter {
@payer(payer) // payer for the "data account"
constructor() {}
// Function to create a new token mint and associated metadata.
@mutableSigner(payer) // payer account
@mutableSigner(mint) // mint account to be created
@mutableAccount(metadata) // metadata account to be created
@signer(mintAuthority) // mint authority for the mint account
@account(rentAddress)
@account(metadataProgramId)
function createTokenMint(
address freezeAuthority, // freeze authority for the mint account
uint8 decimals, // decimals for the mint account
string name, // name for the metadata account
string symbol, // symbol for the metadata account
string uri // uri for the metadata account
) external {
// Invoke System Program to create a new account for the mint account and,
// Invoke Token Program to initialize the mint account
// Set mint authority, freeze authority, and decimals for the mint account
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
);
// Invoke Metadata Program to create a new account for the metadata account
MplMetadata.create_metadata_account(
tx.accounts.metadata.key, // metadata account
tx.accounts.mint.key, // mint account
tx.accounts.mintAuthority.key, // mint authority
tx.accounts.payer.key, // payer
tx.accounts.payer.key, // update authority (of the metadata account)
name, // name
symbol, // symbol
uri, // uri (off-chain metadata json)
tx.accounts.rentAddress.key,
tx.accounts.metadataProgramId.key
);
}
// Function to mint tokens to a specified token account.
@mutableAccount(mint)
@mutableAccount(tokenAccount)
@mutableSigner(mintAuthority)
function mintTo(uint64 amount) external {
// Mint tokens to the token account
SplToken.mint_to(
tx.accounts.mint.key, // mint account
tx.accounts.tokenAccount.key, // token account
tx.accounts.mintAuthority.key, // mint authority
amount // amount
);
}
}
Step 3: Build Your Program
As all contract-related files are ready, go ahead and make sure it compile by running the following command:
anchor build
You should receive a notice that your LLVM IR and Anchor metadata files have been generated. Now, let's write some tests to ensure our program works as expected.
Testing and Deployment
In this section, we will create a test file and then deploy the SPL Token program to the devnet. However, all processes can be applied to the mainnet as well.
Open the my-spl-token.ts
file in the tests
directory.
This testing file tests the functionalities of the spl_token_minter
Solidity smart contract, which is designed to create an SPL token and mint some SPL tokens to your wallet on Solana.
Modify the file corresponding to the code below.
Update the token title, token symbol, and token URI in the highlighted lines to use the information of your own token. You should replace IPFS_URL_OF_JSON_FILE with the IPFS URL of your JSON file that you get in the Upload Token Icon and Metadata to IPFS section.
In this file, the mint amount is specified as 100 My Awesome Tokens. If you want to change it, modify the 99. line in the code below.
Click to see the code explanation
- Configure the Client:
- The testing file configures the client to use the Solana cluster specified in
Anchor.toml
. - It sets up the provider using the
anchor.AnchorProvider.env()
method.
- Generate Keypairs:
- Generates keypairs for the data account (dataAccount) and the mint (mintKeypair).
- The wallet and connection are retrieved from the provider.
- Initialize the Program Data Account:
- The it("Is initialized!") test initializes the data account required by Solang.
- It calls the new method of the program to initialize the data account.
- Create an SPL Token:
- The it("Create an SPL Token!") test creates an SPL token by calling the createTokenMint method.
- It provides necessary parameters such as freeze authority, decimals, token name, symbol, and URI.
- Additionally, it retrieves the metadata address using the Metaplex library.
- The test accounts and signers are specified, and the transaction is executed.
- Mint Tokens to Wallet:
- The it("Mint some tokens to your wallet!") test mints tokens to the user's wallet.
- It first ensures the existence of the associated token account for the wallet.
- The
mintTo
method is then called to mint a specified amount of tokens to the wallet's associated token account. - The necessary accounts are specified, and the transaction is executed.
- Logging Transaction Signatures:
- Throughout the tests, transaction signatures are logged to the console for reference.
You may want to delve deeper into how Anchor maps these functions to methods in the IDL (Interface Description Language) file for
SplTokenMinter
. The IDL file, which is located in the./target/types
directory, provides a structured representation of the smart contract's methods and types, making it a valuable reference.
import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { SplTokenMinter } from '../target/types/spl_token_minter'
import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
getOrCreateAssociatedTokenAccount,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token'
describe('spl-token-minter', () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
// Metaplex Constants
const METADATA_SEED = 'metadata'
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
)
// Generate a new keypair for the data account for the program
const dataAccount = anchor.web3.Keypair.generate()
// Generate a mint keypair
const mintKeypair = anchor.web3.Keypair.generate()
const wallet = provider.wallet as anchor.Wallet
const connection = provider.connection
console.log('Your wallet address', wallet.publicKey.toString())
const program = anchor.workspace.SplTokenMinter as Program<SplTokenMinter>
// Metadata for the Token
const tokenTitle = 'My Awesome Token'
const tokenSymbol = 'MAT'
const tokenUri = 'IPFS_URL_OF_JSON_FILE'
const tokenDecimals = 9
const mint = mintKeypair.publicKey
it('Is initialized!', async () => {
// Initialize data account for the program, which is required by Solang
const tx = await program.methods
.new()
.accounts({ dataAccount: dataAccount.publicKey })
.signers([dataAccount])
.rpc()
console.log('Your transaction signature', tx)
})
it('Create an SPL Token!', async () => {
const [metadataAddress] = PublicKey.findProgramAddressSync(
[
Buffer.from(METADATA_SEED),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)
// Create the token mint
const tx = await program.methods
.createTokenMint(
wallet.publicKey, // freeze authority
tokenDecimals, // decimals
tokenTitle, // token name
tokenSymbol, // token symbol
tokenUri // token uri
)
.accounts({
payer: wallet.publicKey,
mint: mintKeypair.publicKey,
metadata: metadataAddress,
mintAuthority: wallet.publicKey,
rentAddress: SYSVAR_RENT_PUBKEY,
metadataProgramId: TOKEN_METADATA_PROGRAM_ID,
})
.signers([mintKeypair]) // signing the transaction with the keypair, you actually prove that you have the authority to assign the account to the token program
.rpc({ skipPreflight: true })
console.log('Your transaction signature', tx)
})
it('Mint some tokens to your wallet!', async () => {
// Wallet's associated token account address for mint
// To learn more about token accounts, check this guide out. https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-look-up-the-address-of-a-token-account#spl-token-accounts
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer, // payer
mintKeypair.publicKey, // mint
wallet.publicKey // owner
)
const numTokensToMint = new anchor.BN(100)
const decimalTokens = numTokensToMint.mul(
new anchor.BN(10).pow(new anchor.BN(tokenDecimals))
)
const tx = await program.methods
.mintTo(
new anchor.BN(decimalTokens) // amount to mint in Lamports unit
)
.accounts({
mintAuthority: wallet.publicKey,
tokenAccount: tokenAccount.address,
mint: mintKeypair.publicKey,
})
.rpc({ skipPreflight: true })
console.log('Your transaction signature', tx)
})
})
As indicated before, smart contracts are referred to as “programs” on Solana and the @program_id
annotation is used to specify the on-chain address of the program. Now, we need to update the @program_id
in the smart contract.
- Get the
program_id
by running the command below.
anchor keys sync
anchor keys list
Although the project name is my-spl-token, the smart contract name is spl_token_minter as described in the solidity/my-spl-token.sol file.
- Copy the address from your terminal, open your smart contract file, solidity/my-spl-token.sol, and update the
program_id
line with your program ID.
@program_id("YOUR_PROGRAM_ID") // on-chain program address
- Update the
program_id
in theAnchor.toml
file with your program ID that you get in the first step. Also, as we deploy our smart contract to the devnet, change "[programs.localnet]" to "[programs.devnet]"
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.devnet]
my_spl_token = "YOUR_PROGRAM_ID"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "devnet"
wallet = "./id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
- Although it is also possible to use public nodes, we highly recommend using a custom endpoint. If you have created your QuickNode account and get your HTTP provider link in the Set Up Your QuickNode Endpoint section, update this section as well using your HTTP provider link.
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.devnet]
my_spl_token = "YOUR_PROGRAM_ID"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "YOUR_DEVNET_HTTP_PROVIDER_LINK"
wallet = "./id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
Everything is ready to test and deploy!
You can test and deploy your smart contract by running these commands. Since we configured the Anchor.toml file for the devnet, the test file will run on the devnet.
anchor build
anchor test
The test file includes creating an SPL token and then minting some tokens for your wallet. So, as we do the test, the SPL Token is created and minted as well.
If everything goes well, you should see a similar terminal output.
spl-token-minter
Your transaction signature 4G8cnr7q8GpdurPok4JjJX6ZCej5ahdxCuyj2gvxzKq83N8cQczWSCabi4QGBxMVBhNHBb9r3hoTQET5RRMVT6mG
✔ Is initialized! (1157ms)
Your transaction signature 65GKf6M6CiaVMa3CDebFSxvqLYtDMrfMb1QzkztoEzV7c5txuc1wWa3SpWGWXgSvpr1xCK7wStEb28H32xLH8ENx
✔ Create an SPL Token! (344ms)
Your transaction signature 3tYMd7vVS9m3MLnJjx9JFGqMQtmvVYrYWhazLCHGuqC3jwrUiwFvAyppYxuPT81Q4yTiwYuDk5Ja78oTSBGC32oV
✔ Mint some tokens to your wallet! (4376ms)
3 passing (6s)
✨ Done in 7.58s.
Let's check the SPL Token on the Solana Explorer.
Go to the Solana Devnet Explorer.
Search your wallet's public address.
Click on the Tokens tab to see token holdings.
Click on your token and check the token information.
Custom Scripts (optional)
Actually, you don't need to run a test file to run some scripts for operations like minting a token, sending some tokens, etc. Let's learn more about custom scripts in Anchor.
This script deploys a new token and mints some tokens to your account similar to the testing file. So, it's optional.
Creating a Script File
Create a folder scripts
, and a file mint.ts
in it.
mkdir scripts
echo > scripts/mint.ts
Open the mint.ts
file in the scripts
directory.
Modify the file corresponding to the code below.
Update the token title, token symbol, and token URI in the highlighted lines to use the information of your own token as you have done in the previous step.
import * as anchor from '@coral-xyz/anchor'
import { Program } from '@coral-xyz/anchor'
import { SplTokenMinter } from '../target/types/spl_token_minter'
import { PublicKey, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
getOrCreateAssociatedTokenAccount,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token'
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env()
anchor.setProvider(provider)
// Metaplex Constants
const METADATA_SEED = 'metadata'
const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
)
// Generate a new keypair for the data account for the program
const dataAccount = anchor.web3.Keypair.generate()
// Generate a mint keypair
const mintKeypair = anchor.web3.Keypair.generate()
const wallet = provider.wallet as anchor.Wallet
const connection = provider.connection
console.log('Your wallet address', wallet.publicKey.toString())
const program = anchor.workspace.SplTokenMinter as Program<SplTokenMinter>
// Metadata for the Token
const tokenTitle = 'My Awesome Token'
const tokenSymbol = 'MAT'
const tokenUri = 'IPFS_URL_OF_JSON_FILE'
const tokenDecimals = 9
const mint = mintKeypair.publicKey
async function deploy() {
// Initialize data account for the program
const initTx = await program.methods
.new()
.accounts({ dataAccount: dataAccount.publicKey })
.signers([dataAccount])
.rpc()
console.log('Initialization transaction signature', initTx)
const [metadataAddress] = PublicKey.findProgramAddressSync(
[
Buffer.from(METADATA_SEED),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)
// Create the token mint
const createTokenMintTx = await program.methods
.createTokenMint(
wallet.publicKey, // freeze authority
tokenDecimals, // decimals
tokenTitle, // token name
tokenSymbol, // token symbol
tokenUri // token uri
)
.accounts({
payer: wallet.publicKey,
mint: mintKeypair.publicKey,
metadata: metadataAddress,
mintAuthority: wallet.publicKey,
rentAddress: SYSVAR_RENT_PUBKEY,
metadataProgramId: TOKEN_METADATA_PROGRAM_ID,
})
.signers([mintKeypair]) // signing the transaction with the keypair, you actually prove that you have the authority to assign the account to the token program
.rpc({ skipPreflight: true })
console.log('Create Token Mint transaction signature', createTokenMintTx)
// Wallet's associated token account address for mint
// To learn more about token accounts, check this guide out. https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-look-up-the-address-of-a-token-account#spl-token-accounts
const tokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer, // payer
mintKeypair.publicKey, // mint
wallet.publicKey // owner
)
const numTokensToMint = new anchor.BN(100)
const decimalTokens = numTokensToMint.mul(
new anchor.BN(10).pow(new anchor.BN(tokenDecimals))
)
const mintTx = await program.methods
.mintTo(
new anchor.BN(decimalTokens) // amount to mint in Lamports unit
)
.accounts({
mintAuthority: wallet.publicKey,
tokenAccount: tokenAccount.address,
mint: mintKeypair.publicKey,
})
.rpc({ skipPreflight: true })
console.log('Mint Tokens transaction signature', mintTx)
}
// Run the deployment script
deploy().catch(err => console.error(err))
Adding a Command
Open the Anchor.toml
file, and modify the [scripts] section as seen below. We basically add a mint command that runs the scripts/mint.ts
file.
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
mint = "yarn ts-node scripts/mint.ts"
Running the Script
Run the script.
anchor run mint
The output should be similar to the one below.
Initialization transaction signature Xq1uwvDsBPY1TQh421r6ryBi82CqsKaHfzojXuaRgppKFFW5vvk98BD6HrcagrkyU8szJYim3Ldm9fRcM7R1F2F
Create Token Mint transaction signature 2JkBZQPT6WRSEx63iLej1dC4xJFiYvNLcFNPWaN2eJKaHoEmMqWcEDw3RpPDFbufim66U1cVRYCc1zSM6b6Mhixk
Mint Tokens transaction signature 4XBdYRpUiUvD2UEVFUdkBMyhB6pLFwkwbuME7FoeFKnFFjwJXTnKqzMJe6takM6aZsjL5AiCTTgqCKWT4SWyL18X
✨ Done in 8.65s.
Conclusion
Mega job! You just successfully created a Solana program using Solidity and minted your own token on Solana using the new Metaplex fungible token standard.
If you have any questions or need further assistance, feel free to join our Discord server or provide feedback using the form below. Stay up to date with the latest by following us on Twitter (@QuickNode) and our Telegram announcement channel.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.