The MemoTransfer
extension enforces that every incoming transfer to a Token
Account is accompanied by a memo instruction.
This memo instruction records a message in the transaction's program logs. This
feature is particularly useful for adding context to transactions, making it
easier to understand their purpose when reviewing the transaction logs later.
In this guide, we'll walk through an example of using Solana Playground. Here is the final script.
Getting Started
Start by opening this Solana Playground link with the following starter code.
// Clientconsole.log("My address:", pg.wallet.publicKey.toString());const balance = await pg.connection.getBalance(pg.wallet.publicKey);console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);
If it is your first time using Solana Playground, you'll first need to create a Playground Wallet and fund the wallet with devnet SOL.
If you do not have a Playground wallet, you may see a type error within the
editor on all declarations of pg.wallet.publicKey
. This type error will clear
after you create a Playground wallet.
To get devnet SOL, run the solana airdrop
command in the Playground's
terminal, or visit this devnet faucet.
solana airdrop 5
Once you've created and funded the Playground wallet, click the "Run" button to run the starter code.
Add Dependencies
Let's start by setting up our script. We'll be using the @solana/web3.js
,
@solana/spl-token
, and @solana/spl-memo
libraries.
Replace the starter code with the following:
import {Connection,Keypair,SystemProgram,Transaction,clusterApiUrl,sendAndConfirmTransaction,TransactionInstruction,PublicKey,} from "@solana/web3.js";import {ExtensionType,TOKEN_2022_PROGRAM_ID,createEnableRequiredMemoTransfersInstruction,createInitializeAccountInstruction,createMint,disableRequiredMemoTransfers,enableRequiredMemoTransfers,getAccountLen,createAccount,mintTo,createTransferInstruction,} from "@solana/spl-token";import { createMemoInstruction } from "@solana/spl-memo";// Playground walletconst payer = pg.wallet.keypair;// Connection to devnet clusterconst connection = new Connection(clusterApiUrl("devnet"), "confirmed");// Transaction to sendlet transaction: Transaction;// Transaction signature returned from sent transactionlet transactionSignature: string;
Mint Setup
We'll first need to create a new Mint Account before we can create Token Accounts.
// Authority that can mint new tokensconst mintAuthority = pg.wallet.publicKey;// Decimals for Mint Accountconst decimals = 2;// Create Mint Accountconst mint = await createMint(connection,payer, // Payer of the transaction and initialization feesmintAuthority, // Mint Authoritynull, // Optional Freeze Authoritydecimals, // Decimals of Mintundefined, // Optional keypairundefined, // Options for confirming the transactionTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Memo Transfer Token Account
Next, let's build a transaction to enable the MemoTransfer
extension for a new
Token Account.
First, let's generate a new keypair to use as the address of the Token Account.
// Random keypair to use as owner of Token Accountconst tokenAccountKeypair = Keypair.generate();// Address for Token Accountconst tokenAccount = tokenAccountKeypair.publicKey;
Next, let's determine the size of the new Token Account and calculate the minimum lamports needed for rent exemption.
// Size of Token Account with extensionconst accountLen = getAccountLen([ExtensionType.MemoTransfer]);// Minimum lamports required for Token Accountconst lamports = await connection.getMinimumBalanceForRentExemption(accountLen);
With Token Extensions, the size of the Token Account will vary based on the extensions enabled.
Build Instructions
Next, let's build the set of instructions to:
- Create a new account
- Initialize the Token Account data
- Enable the
MemoTransfer
extension
First, build the instruction to invoke the System Program to create an account and assign ownership to the Token Extensions Program.
// Instruction to invoke System Program to create new accountconst createAccountInstruction = SystemProgram.createAccount({fromPubkey: payer.publicKey, // Account that will transfer lamports to created accountnewAccountPubkey: tokenAccount, // Address of the account to createspace: accountLen, // Amount of bytes to allocate to the created accountlamports, // Amount of lamports transferred to created accountprogramId: TOKEN_2022_PROGRAM_ID, // Program assigned as owner of created account});
Next, build the instruction to initialize the Token Account data.
// Instruction to initialize Token Account dataconst initializeAccountInstruction = createInitializeAccountInstruction(tokenAccount, // Token Account Addressmint, // Mint Accountpayer.publicKey, // Token Account OwnerTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Lastly, build the instruction to enable the MemoTransfer
extension for the
Token Account.
// Instruction to initialize the MemoTransfer Extensionconst enableRequiredMemoTransfersInstruction =createEnableRequiredMemoTransfersInstruction(tokenAccount, // Token Account addresspayer.publicKey, // Token Account Ownerundefined, // Additional signersTOKEN_2022_PROGRAM_ID, // Token Program ID);
Send Transaction
Next, let's add the instructions to a new transaction and send it to the
network. This will create a Token Account with the MemoTransfer
extension
enabled.
// Add instructions to new transactiontransaction = new Transaction().add(createAccountInstruction,initializeAccountInstruction,enableRequiredMemoTransfersInstruction,);// Send transactiontransactionSignature = await sendAndConfirmTransaction(connection,transaction,[payer, tokenAccountKeypair], // Signers);console.log("\nCreate Token Account:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Run the script by clicking the Run
button. You can then inspect the
transaction details on SolanaFM.
Create and Fund Token Account
Next, let's set up another Token Account to demonstrate the functionality of the
MemoTransfer
extension.
First, create a sourceTokenAccount
owned by the Playground wallet.
// Create Token Account for Playground walletconst sourceTokenAccount = await createAccount(connection,payer, // Payer to create Token Accountmint, // Mint Account addresspayer.publicKey, // Token Account ownerundefined, // Optional keypair, default to Associated Token Accountundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Next, mint 2 tokens to the sourceTokenAccount
to fund it.
// Mint tokens to sourceTokenAccounttransactionSignature = await mintTo(connection,payer, // Transaction fee payermint, // Mint Account addresssourceTokenAccount, // Mint tomintAuthority, // Mint Authority address200, // Amountundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nMint Tokens:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Transfer and Memo Instruction
Next, let's prepare the token transfer and memo instructions.
First, build the instruction to transfer tokens from the sourceTokenAccount
to
the tokenAccount
which has the MemoTransfer
extension enabled.
// Instruction to transfer tokensconst transferInstruction = createTransferInstruction(sourceTokenAccount, // Source Token AccounttokenAccount, // Destination Token Accountpayer.publicKey, // Source Token Account owner100, // Amountundefined, // Additional signersTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);
Next, build the memo instruction. The message will be included in the program logs of the transaction the instruction is added to.
// Message for the memoconst message = "Hello, Solana";// Instruction to add memoconst memoInstruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],data: Buffer.from(message, "utf-8"),programId: new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),});
Alternatively, you can create the instruction using the @solana/spl-memo
library:
// Message for the memoconst message = "Hello, Solana";// Instruction to add memoconst memoInstruction = createMemoInstruction(message, [payer.publicKey]);
Attempt Transfer without Memo
To demonstrate the functionality of the MemoTransfer
extension, let's first
attempt to send a token transfer without a memo.
try {// Attempt to transfer without memotransaction = new Transaction().add(transferInstruction);// Send transactionawait sendAndConfirmTransaction(connection,transaction,[payer], // Signers);} catch (error) {console.log("\nExpect Error:", error);}
Run the script by clicking the Run
button. You can then inspect the error in
the Playground terminal. You should see a message similar to the following:
Expect Error: { [Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x24]logs:[ 'Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]','Program log: Instruction: Transfer','Program log: Error: No memo in previous instruction; required for recipient to receive a transfer','Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb consumed 6571 of 200000 compute units','Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb failed: custom program error: 0x24' ] }
Transfer with Memo
Next, send a token transfer with the memo instruction included on the transaction.
// Add instructions to new transactiontransaction = new Transaction().add(memoInstruction, transferInstruction);// Send transactiontransactionSignature = await sendAndConfirmTransaction(connection,transaction,[payer], // Signers);console.log("\nTransfer with Memo:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Run the script by clicking the Run
button. You can then inspect the
transaction details on SolanaFM.
Enable and Disable Memo Transfer
The MemoTransfer
extension can also be freely enabled or disabled by the Token
Account owner.
To enable the MemoTransfer
extension, use the enableRequiredMemoTransfers
instruction.
// Enable Required Memo TransferstransactionSignature = await enableRequiredMemoTransfers(connection, // Connection to usepayer, // Payer of the transaction feetokenAccount, // Token Account to modifypayer.publicKey, // Owner of Token Accountundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nEnable Required Memo Transfers:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
To disable the MemoTransfer
extension, use the disableRequiredMemoTransfers
instruction.
// Disable Required Memo TransferstransactionSignature = await disableRequiredMemoTransfers(connection, // Connection to usepayer, // Payer of the transaction feetokenAccount, // Token Account to modifypayer.publicKey, // Owner of Token Accountundefined, // Additional signersundefined, // Confirmation optionsTOKEN_2022_PROGRAM_ID, // Token Extension Program ID);console.log("\nDisable Required Memo Transfers:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Once the MemoTransfer
extension is disabled, transactions to transfer tokens
without a memo instruction will complete successfully.
// Add instructions to new transactiontransaction = new Transaction().add(transferInstruction);// Send transactiontransactionSignature = await sendAndConfirmTransaction(connection,transaction,[payer], // Signers);console.log("\nTransfer without Memo:",`https://solana.fm/tx/${transactionSignature}?cluster=devnet-solana`,);
Run the script by clicking the Run
button. You can then inspect the
transaction details on SolanaFM.
Conclusion
The MemoTransfer
extension ensures every incoming transfer to a Token Account
includes a memo. By requiring a memo instruction with each transfer, a message
is recorded in the transaction's program logs. This feature is especially useful
for understanding the purpose of transactions when reviewing logs at a later
time.