How I Built a Node App to Transfer Custom Tokens on Solana Blockchain

Siddhartha Satyakama
Coinmonks
7 min readApr 21, 2023

--

Hey guys, I am excited to share with you my first ever blockchain mini-project, a basic interface to transfer custom tokens on the Solana blockchain! 🚀

Inspiration

I was checking videos on blockchain and specifically what Solana project is doing, and that inspired me to get into this project. I wanted to explore the power of blockchain and develop something useful for the community.

Challenges Faced

While developing this project, I faced a few challenges in understanding the concepts of how transactions over the Solana blockchain actually works. But, with the help of more videos and articles, I was able to understand the basic fundamentals of all the small units and terms used.

Project Overview

To create this project, I started with a node app and used the solana/web3.js library to connect to the Solana devnet and interact with the blockchain.

npm init -y
npm install express ejs @solana/web3.js bs58 body-parser axios

Next, I created an ejs file with a basic HTML form and some CSS stylings. The form asks for the recipient’s public key, the amount of custom token to transfer and select the custom token to send.

<!DOCTYPE html>
<html>
<head>
<title>Solana Blockchain</title>
<link rel="stylesheet" type="text/css" href="/styles.css">
</head>
<body>
<div class="container">
<h1>Transfer Tokens</h1>
<form>
<label for="recipient-public-key">Recipient public key:</label>
<input type="text" id="recipient-public-key" name="public-key" placeholder="Enter recipient public key">

<label for="amount">Amount:</label>
<input type="text" id="amount" name="amount" placeholder="Enter amount">

<label for="token">Select token:</label>
<select id="token" name="token">
<option value="SOL">Solana</option>
<option value="ETH">Ethereum</option>
<option value="USDT">Tether USD</option>
</select>

<input type="button" onclick="submitForm(event)" value="Send Payment" id="submitButton">
<span id="message"></span>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/script.js"></script>
</body>
</html>

After that, I created transferCustomToken and transferSOLToken function that takes the recipient’s public key, the transfer amount and the custom token name as parameters. This function connects to the Solana devnet and transfers the tokens from the sender’s wallet to the recipient’s address.

Hey, now we come to the most crucial part of this article — understanding the implementation behind transferring tokens on the Solana blockchain. This is where we’ll dive into the nitty-gritty details of how it all works, so buckle up and let’s get started! 🥳

PART I: Implementation of transferCustomToken

The transferCustomToken function is designed to transfer custom tokens on the Solana blockchain network from one account to another. This function uses the @solana/web3.js and @solana/spl-token libraries for interacting with the Solana network and working with tokens.

The function takes three parameters — recipientPublicKeyString, amount, and token. The recipientPublicKeyString is the public key of the account receiving the tokens, amount is the number of tokens being transferred, and token is the symbol or name of the token being transferred.

async function transferCustomToken(recipientPublicKeyString, amount, token) {
// Write your code here
}

The first step in transferring the custom token is to connect to the Solana network. This is done using the Connection class from @solana/web3.js library. In this implementation, the function is connecting to the Solana Devnet cluster.

async function transferCustomToken(recipientPublicKeyString, amount, token) {
// Connect to cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
}

Next, the function fetches the private key of the account sending the custom token. The private key is stored in the senderPrivateKeyString variable, which is retrieved from an environment variable.

The function then creates a Keypair object from the private key and retrieves the public key of the recipient's account. These keys are used later in the function to send and receive the custom token.

async function transferCustomToken(recipientPublicKeyString, amount, token) {
// Connect to cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

// Fetching my wallet from private key
const senderPrivateKeyString = process.env.WALLET_SECRET_KEY;
const senderPrivateKey = bs58.decode(senderPrivateKeyString);
const fromWallet = Keypair.fromSecretKey(senderPrivateKey);
}

The getTokenMint function is called to get the mint address of the custom token being transferred. The getTokenMint function takes three parameters - the public key of the sender's account, the Solana network connection, and the name or symbol of the token being transferred. This function uses the getParsedProgramAccounts method of the Connection class to retrieve all accounts associated with the TOKEN_PROGRAM_ID, which is the program responsible for managing custom tokens on the Solana network. The function filters the accounts based on the sender's public key and the token's decimals. Finally, the function returns the mint address of the token.

const tokenToDecimal = {
USDT: 6,
ETH: 18,
};

async function getTokenMint(wallet, solanaConnection, token) {
const filters = [
{
dataSize: 165, //size of account (bytes)
},
{
memcmp: {
offset: 32, //location of our query in the account (bytes)
bytes: wallet, //our search criteria, a base58 encoded string
},
},
];
const accounts = await solanaConnection.getParsedProgramAccounts(
TOKEN_PROGRAM_ID,
{ filters: filters }
);
for (let i = 0; i < accounts.length; i++) {
const account = accounts[i];
//Parse the account data
const parsedAccountInfo = account.account.data;
const decimals =
parsedAccountInfo["parsed"]["info"]["tokenAmount"]["decimals"];
if (decimals === tokenToDecimal[token]) {
return parsedAccountInfo["parsed"]["info"]["mint"];
}
}
// If no account is found, return null
return null;
}

Once the mint address is retrieved, the function creates PublicKey objects for the mint and recipient's account. These objects are used to create or retrieve associated token accounts for the sender and recipient using the getOrCreateAssociatedTokenAccount function. The getOrCreateAssociatedTokenAccount function takes four parameters - the Solana network connection, the sender's Keypair object, the mint public key, and the owner's public key. This function creates an associated token account if it does not already exist and returns its public key.

Finally, the transfer function is called to transfer the custom token from the sender's associated token account to the recipient's associated token account. The transfer function takes six parameters - the Solana network connection, the sender's Keypair object, the sender's associated token account public key, the recipient's associated token account public key, the sender's public key, and the amount of tokens being transferred. This function returns a signature object that can be used to track the status of the transaction on the Solana network.

async function transferCustomToken(recipientPublicKeyString, amount, token) {
try {
// Connect to cluster
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

// Fetching my wallet from private key
const senderPrivateKeyString = process.env.WALLET_SECRET_KEY;
const senderPrivateKey = bs58.decode(senderPrivateKeyString);
const fromWallet = Keypair.fromSecretKey(senderPrivateKey);

// Get public key for receiver public key
const toWallet = new PublicKey(recipientPublicKeyString);

// Get token mint public key
const tokenAddress = await getTokenMint(
fromWallet.publicKey,
connection,
token
);
if (tokenAddress === null) {
throw Error("Token mint address not found!");
}
const mint = new PublicKey(tokenAddress);

// Get the token account of the fromWallet address, and if it does not exist, create it
const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
fromWallet,
mint,
fromWallet.publicKey
);

// Get the token account of the toWallet address, and if it does not exist, create it
const toTokenAccount = await getOrCreateAssociatedTokenAccount(
connection,
fromWallet,
mint,
toWallet
);

// Transfer the custom token to the "toTokenAccount" we just created
const amountToTransfer = amount * Math.pow(10, tokenToDecimal[token]);
signature = await transfer(
connection,
fromWallet,
fromTokenAccount.address,
toTokenAccount.address,
fromWallet.publicKey,
amountToTransfer
);
} catch (e) {
throw Error(e);
}
}

Overall, the transferCustomToken function provides a simple and efficient way to transfer custom tokens on the Solana network. It abstracts away much of the complexity involved in working with Solana's programmatic interfaces and allows developers to easily build custom token transfer functionality into their applications.

PART II: Implementation of transferSOLToken

The transferSOLToken function takes in two parameters: the recipient's public key as a string and the amount of tokens to transfer. It then attempts to transfer the specified amount of tokens to the recipient's public key.

async function transferSOLToken(recipientPublicKeyString, amount) {
try {
// Code for transferring tokens
} catch (e) {
throw Error('Transaction failed');
}
}

A connection is established with the Solana network provider to allow the function to interact with the blockchain.

const connection = new solanaWeb3.Connection(
solanaWeb3.clusterApiUrl("devnet")
);

The function loads the sender’s wallet by retrieving the private key from an environment variable and decoding it using Base58 encoding. The private key is then used to create a keypair object that will be used for signing the transaction.

const senderPrivateKeyString = process.env.WALLET_SECRET_KEY;
const senderPrivateKey = bs58.decode(senderPrivateKeyString);
const senderKeypair = solanaWeb3.Keypair.fromSecretKey(senderPrivateKey);

The function creates a new PublicKey object for the recipient's public key and converts the specified amount of tokens into lamports.

const recipientPublicKey = new solanaWeb3.PublicKey(recipientPublicKeyString);
const lamportsToSend = amount * solanaWeb3.LAMPORTS_PER_SOL;

A new transaction is created using the Transaction object provided by @solana/web3.js. The transfer method of the SystemProgram is used to specify the transfer of tokens from the sender's public key to the recipient's public key.

const transaction = new solanaWeb3.Transaction().add(
solanaWeb3.SystemProgram.transfer({
fromPubkey: senderKeypair.publicKey,
toPubkey: recipientPublicKey,
lamports: lamportsToSend,
})
);

Finally, the function signs the transaction with the sender’s keypair and sends it to the network provider for confirmation. If successful, a message is logged to the console indicating that the transaction was successful.

await solanaWeb3.sendAndConfirmTransaction(connection, transaction, [senderKeypair]);
console.log("Transaction successful!");

To make it easy for other developers to use my project, I wired up the transferCustomToken and transferSOLToken function with a POST API. This way, anyone can send a transfer request to my server and have their tokens transferred automatically.

/* Transfer Tokens */
app.post('/transfer-tokens', async (req, res, next) => {
try {
const { recipientPublicKey, amount, token } = req.body;
if (token === 'SOL') {
await transferSOLToken(recipientPublicKey, amount);
} else {
await transferCustomToken(recipientPublicKey, amount, token);
}
res.status(201).json({
'status': 'success',
'details': 'Token transferred successfully'
});
} catch(error) {
next(error);
}
});

On the client-side, I used axios to create a function that calls the /transfer-tokens API with the recipient’s public key and the transfer amount as parameters. It’s super easy to use! 😎

function transferTokenToRecipient(recipientPublicKey, amount, token) {
axios.post('/transfer-tokens', {
recipientPublicKey: recipientPublicKey,
amount: amount,
token: token
})
.then(function (response) {
var messageEl = document.getElementById('message');
messageEl.textContent = `${amount} ${token} transferred to ${recipientPublicKey}`;
messageEl.style.color = 'green';
document.getElementById('submitButton').value = 'Send Payment';
})
.catch(function (error) {
var messageEl = document.getElementById('message');
messageEl.textContent = 'Transaction failed!';
messageEl.style.color = 'red';
document.getElementById('submitButton').value = 'Send Payment';
});
}

Benefits for Developers

Developers who have a little bit of understanding of how Node.js app works can explore the implementation of transacting custom tokens. This will help them with the basics of the Solana blockchain.

Conclusion

In conclusion, with this project, I want to inspire other budding blockchain developers and students to explore this field and learn about the power of blockchain. Give it a try and let me know what you think! ✨

If you’re interested in trying out the project, here’s the GitHub repo — https://github.com/mrsidrdx/Solana-Web3JS

Also, I have hosted my Node.js app on Render — https://solana-web3.onrender.com.

--

--

Siddhartha Satyakama
Coinmonks

I'm a software engineer passionate about building impactful products, I thrive on using my full-stack development expertise to deliver seamless user experiences