19 min read
Overview
Wrapped SOL (wSOL) is a token on the Solana blockchain that represents native SOL in the form of an SPL token. This guide will teach you how to interact with wrapped SOL using the Solana-Web3.js JavaScript library, Solana CLI, and the Solana Rust SDK, including wrapping, transferring, and unwrapping operations.
Let's get started!
What You Will Do
Write a script to perform the following operations using Solana Web3.js, Solana CLI, and the Solana Rust SDK:
- Wrap SOL into wSOL
- Transfer wrapped SOL to another wallet
- Unwrap SOL
What You Will Need
- Nodejs (version 16.15 or higher) installed
- TypeScript experience and ts-node installed
- Solana CLI installed
- SPL Token CLI installed
- Understanding of Solana Basics
- Understanding of SPL Tokens
- Rust installed (optional for Rust method)
Dependencies Used in this Guide
Dependency | Version |
---|---|
@solana/web3.js | ^1.91.1 |
@solana/spl-token | ^0.3.9 |
typescript | ^5.0.0 |
ts-node | ^10.9.1 |
solana cli | 1.18.8 |
spl-token cli | >3.3.0 |
rust | 1.75.0 |
Let's get started!
What is Wrapped SOL?
Wrapped SOL (wSOL) is a token on the Solana blockchain that represents native SOL in the form of an SPL token (the native token protocol on Solana). It allows you to use SOL in contexts that require SPL tokens, such as decentralized exchanges or other DeFi applications that interact with token pools. By wrapping SOL, you can integrate native SOL seamlessly with other SPL tokens in their applications, enabling a wider range of functionalities and interactions within the Solana ecosystem. This flexibility makes wrapped SOL an essential tool for creating more complex and interoperable decentralized applications on Solana.
To wrap SOL, all you need to do is send SOL to an associated token account on the native mint (So11111111111111111111111111111111111111112
: which is accessible via the NATIVE_MINT
constant in the @solana/spl-token
package) and calling the syncNative instruction (source) from the SPL token program. syncNative
updates the amount field on the token account to match the amount of wrapped SOL available. That SOL is only retrievable by closing the token account and choosing the desired address to send the token account's lamports.
Let's create a sample script demonstrating the full lifecycle of wrapped SOL transactions.
Solana-Web3.js
Create a New Project
First, let's set up our project:
mkdir wrapped-sol-demo && cd wrapped-sol-demo && echo > app.ts
Initialize a new project with your favorite package manager (we'll use npm in this guide):
npm init -y
And install the necessary dependencies:
npm install @solana/web3.js @solana/spl-token
Set Up Local Environment
Import Dependencies
Open app.ts
and import the required dependencies:
import {
NATIVE_MINT,
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddress,
createSyncNativeInstruction,
getAccount,
createTransferInstruction,
createCloseAccountInstruction,
} from "@solana/spl-token";
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
PublicKey,
} from "@solana/web3.js";
These imports provide the necessary functions and types for interacting with the Solana blockchain and the SPL token program.
Create Main Function
Now, go ahead and define a main
function that will orchestrate our operations. Add the following code to app.ts
below your imports:
async function main() {
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
const wallet1 = Keypair.generate();
const wallet2 = Keypair.generate();
await requestAirdrop(connection, wallet1);
const tokenAccount1 = await wrapSol(connection, wallet1);
const tokenAccount2 = await transferWrappedSol(connection, wallet1, wallet2, tokenAccount1);
await unwrapSol(connection, wallet1, tokenAccount1);
await printBalances(connection, wallet1, wallet2, tokenAccount2);
}
This function outlines our steps and calls several functions to perform each step. We will define each function in the next section. For this example, we are going to use a local test validator to run our code; however, you can modify the connection to work with devnet or mainnet for real-world applications. When you are ready to start interacting with devnet or mainnet, you will need an RPC endpoint. You can get one for free at QuickNode.com, and then you can just change the connection to point to your endpoint like this:
const connection = new Connection("https://example.solana-mainnet.quiknode.pro/123/", "confirmed");
Create Wrapped SOL Operations
Let's create functions for each operation we want to perform.
1. Request Airdrop
Add the following function to request an airdrop of SOL:
async function requestAirdrop(connection: Connection, wallet: Keypair): Promise<void> {
const airdropSignature = await connection.requestAirdrop(
wallet.publicKey,
2 * LAMPORTS_PER_SOL
);
while (true) {
const { value: statuses } = await connection.getSignatureStatuses([fromAirdropSignature]);
if (!statuses || statuses.length === 0) {
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
if (statuses[0] && statuses[0].confirmationStatus === 'confirmed' || statuses[0].confirmationStatus === 'finalized') {
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
} console.log("✅ - Step 1: Airdrop completed");
}
This function requests a 2 SOL airdrop to the specified wallet.
2. Wrap SOL
Next, let's create a function to wrap SOL:
async function wrapSol(connection: Connection, wallet: Keypair): Promise<PublicKey> {
const associatedTokenAccount = await getAssociatedTokenAddress(
NATIVE_MINT,
wallet.publicKey
);
const wrapTransaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
wallet.publicKey,
associatedTokenAccount,
wallet.publicKey,
NATIVE_MINT
),
SystemProgram.transfer({
fromPubkey: wallet.publicKey,
toPubkey: associatedTokenAccount,
lamports: LAMPORTS_PER_SOL,
}),
createSyncNativeInstruction(associatedTokenAccount)
);
await sendAndConfirmTransaction(connection, wrapTransaction, [wallet]);
console.log("✅ - Step 2: SOL wrapped");
return associatedTokenAccount;
}
Let's walk through this function:
- First, we define an associated token account seeded on the wallet's public key and the native mint.
- Next, we assemble a transaction that has three instructions:
- We create the associated token account for wrapped SOL.
- We transfer 1 SOL to the newly created ATA.
- We call the
syncNative
instruction to update the amount field on the ATA to match the amount of wrapped SOL available.
- Finally, we send the transaction and log a success message.
3. Transfer Wrapped SOL
Now, let's create a function to transfer wrapped SOL:
async function transferWrappedSol(
connection: Connection,
fromWallet: Keypair,
toWallet: Keypair,
fromTokenAccount: PublicKey
): Promise<PublicKey> {
const toTokenAccount = await getAssociatedTokenAddress(
NATIVE_MINT,
toWallet.publicKey
);
const transferTransaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
fromWallet.publicKey,
toTokenAccount,
toWallet.publicKey,
NATIVE_MINT
),
createTransferInstruction(
fromTokenAccount,
toTokenAccount,
fromWallet.publicKey,
LAMPORTS_PER_SOL / 2
)
);
await sendAndConfirmTransaction(connection, transferTransaction, [fromWallet]);
console.log("✅ - Step 3: Transferred wrapped SOL");
return toTokenAccount;
}
Since wrapped SOL is just an SPL token, this function is just a simple SPL token transfer (check our guide: How to Transfer SPL Tokens on Solana). This function creates an ATA for the recipient (seeded on the NATIVE_MINT
), then transfers half of the wrapped SOL to it.
4. Unwrap SOL
Let's add a function to unwrap SOL. To do this, all we need to do is close the associated token account and choose the desired address to send the token account's lamports.
async function unwrapSol(
connection: Connection,
wallet: Keypair,
tokenAccount: PublicKey
): Promise<void> {
const unwrapTransaction = new Transaction().add(
createCloseAccountInstruction(
tokenAccount,
wallet.publicKey,
wallet.publicKey
)
);
await sendAndConfirmTransaction(connection, unwrapTransaction, [wallet]);
console.log("✅ - Step 4: SOL unwrapped");
}
This function closes the wrapped SOL associated token account, effectively unwrapping the SOL. We choose the destination for the lamports by passing in the wallet's public key.
5. Print Balances
Finally, let's add a function to print the balances:
async function printBalances(
connection: Connection,
wallet1: Keypair,
wallet2: Keypair,
tokenAccount2: PublicKey
): Promise<void> {
const [wallet1Balance, wallet2Balance, tokenAccount2Info] = await Promise.all([
connection.getBalance(wallet1.publicKey),
connection.getBalance(wallet2.publicKey),
connection.getTokenAccountBalance(tokenAccount2)
]);
console.log(` - Wallet 1 SOL balance: ${wallet1Balance / LAMPORTS_PER_SOL}`);
console.log(` - Wallet 2 SOL balance: ${wallet2Balance / LAMPORTS_PER_SOL}`);
console.log(` - Wallet 2 wrapped SOL: ${Number(tokenAccount2Info.value.amount) / LAMPORTS_PER_SOL}`);
}
This function fetches and displays the SOL and wrapped SOL balances for both wallets.
Run the Script
Finally, add a call to main
at the bottom of your app.ts
file:
main().catch(console.error);
To run our code on localnet, open a terminal and start a local Solana validator:
solana-test-validator -r
Once the validator has started, in another terminal, run your script:
ts-node app.ts
You should see the output of each step in your terminal. We should expect that Wallet 1 has ~1.5 SOL (2 SOL airdropped, minus 1 SOL transferred, plus 0.5 SOL unwrapped, less rent and transaction fees), Wallet 2 has 0 SOL (because we did not send any SOL to Wallet 2), and Wallet 2 has 0.5 wrapped SOL:
ts-node app
✅ - Step 1: Airdrop completed
✅ - Step 2: SOL wrapped
✅ - Step 3: Transferred wrapped SOL
✅ - Step 4: SOL unwrapped
- Wallet 1 SOL balance: 1.49794572
- Wallet 2 SOL balance: 0
- Wallet 2 wrapped SOL: 0.5
Great job!
Solana CLI
Let's try to do the same thing using the Solana CLI. Make sure you have the latest version of Solana CLI and SPL Token CLI installed by running:
solana -V
and
spl-token -V
If you have any issues with commands in this section, you can always run solana COMMAND_NAME -h
or spl-token COMMAND_NAME -h
to get help using the CLI.
Set Up Local Environment
First, let's make sure your CLI is configured to use localnet:
solana config set -ul
Now, let's create keypairs for wallet 1 and wallet 2 (you can do this in the same JS project directory to keep things simple):
solana-keygen new --no-bip39-passphrase -s -o wallet1.json
and
solana-keygen new --no-bip39-passphrase -s -o wallet2.json
You should see two files created in your project directory: wallet1.json
and wallet2.json
. These files contain the private keys for the two wallets we will use in this example.
Airdrop SOL
In your terminal, airdrop 2 SOL to wallet1.json
:
solana airdrop -k wallet1.json 2
Since we are using a relative path, make sure your terminal is in the same directory as wallet1.json
(this should be the case if you followed the instructions in this guide).
Wrap SOL
To wrap SOL with the CLI, you just need to use the spl-token wrap
command. Just like our previous example, we will wrap 1 SOL from wallet1.json
into wSOL:
spl-token wrap 1 wallet1.json
This command will create a new token account for wSOL and mint 1 wSOL to the wallet.
Transfer Wrapped SOL
To use the CLI to transfer SPL tokens like wSOL, you must first create an associated token account for the recipient. This is done using the spl-token create-account
command. Let's create an associated token account for wallet2.json
:
spl-token create-account So11111111111111111111111111111111111111112 --owner wallet2.json --fee-payer wallet1.json
Note that we are using the NATIVE_MINT
as the token mint for the associated token account, and we are using wallet1.json
as the fee payer. The terminal should output the address of the new associated token account, e.g., Creating account 2MhZKqyeLY1X2g7jFb7CQ4D4ySHMvGkTfUYZFWGe7fXw
. We will need this in just a moment.
Now, let's transfer 0.5 wSOL from wallet1.json
to the new associated token account we created (replace 2MhZKqyeLY1X2g7jFb7CQ4D4ySHMvGkTfUYZFWGe7fXw
with the address of the new associated token account you created):
spl-token transfer So11111111111111111111111111111111111111112 0.5 2MhZKqyeLY1X2g7jFb7CQ4D4ySHMvGkTfUYZFWGe7fXw --owner wallet1.json --fee-payer wallet1.json
This command transfers 0.5 wSOL from wallet1.json
to the new associated token account. The --owner
flag specifies the wallet that will own the source wSOL account, and the --fee-payer
flag specifies the wallet that will pay the transaction fee.
Unwrap SOL
To unwrap wSOL, you can use the spl-token unwrap
command. Let's unwrap our wSOL from wallet1.json
by closing the associated token account. In your terminal, run:
spl-token unwrap --owner wallet1.json
This command will unwrap the wSOL from the associated token account and send the wrapped SOL to the wallet that owns the associated token account.
Check Balances
Finally, we can double-check our balances to ensure everything worked as expected. In your terminal, run:
solana balance -k wallet1.json
We expect this to show a balance of ~1.5 SOL.
solana balance -k wallet2.json
We expect this to show a balance of 0.
spl-token balance --address 2MhZKqyeLY1X2g7jFb7CQ4D4ySHMvGkTfUYZFWGe7fXw
Replace the address with the address of the new associated token account you created. We expect this to show a balance of 0.5.
Great job! You now know how to use the Solana CLI to wrap, transfer, unwrap, and check balances.
Rust
For developers who prefer working with Rust, here's how you can perform the same wrapped SOL operations using the Solana Rust SDK.
Set Up the Project
First, create a new Rust project:
cargo new wrapped-sol-rust
cd wrapped-sol-rust
Add the following dependencies to your Cargo.toml
file:
[dependencies]
solana-sdk = "2.0.3"
solana-client = "2.0.3"
spl-token = "6.0.0"
spl-associated-token-account = "4.0.0"
Create Main Function
Let's try and recreate the JavaScript code in Rust. Let's start by importing the necessary crates and creating a main function that outlines the steps of airdropping, wrapping, transferring, and unwrapping SOL. Inside of src/main.rs
, replace the contents with the following code:
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
pubkey::Pubkey,
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
};
use spl_associated_token_account::get_associated_token_address;
use spl_token::{instruction as spl_instruction, native_mint};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rpc_client = RpcClient::new_with_commitment(
"http://127.0.0.1:8899".to_string(),
CommitmentConfig::processed(),
);
let wallet1 = Keypair::new();
let wallet2 = Keypair::new();
request_airdrop(&rpc_client, &wallet1)?;
let token_account1 = wrap_sol(&rpc_client, &wallet1)?;
let token_account2 = transfer_wrapped_sol(&rpc_client, &wallet1, &wallet2, &token_account1)?;
unwrap_sol(&rpc_client, &wallet1, &token_account1)?;
print_balances(&rpc_client, &wallet1, &wallet2, &token_account2)?;
Ok(())
}
This function outlines our steps and calls several functions to perform each step. We will define each function in the next section. For this example, we are going to use a local test validator to run our code; however, you can modify the connection to work with devnet or mainnet for real-world applications. When you are ready to start interacting with devnet or mainnet, you will need an RPC endpoint. You can get one for free at QuickNode.com, and then you can just change the connection to point to your endpoint like this:
let rpc_client = RpcClient::new_with_commitment(
"https://example.solana-mainnet.quiknode.pro/123/".to_string(),
CommitmentConfig::processed(),
);
Create Wrapped SOL Operations
Let's create functions for each operation we want to perform.
1. Request Airdrop
fn request_airdrop(
rpc_client: &RpcClient,
wallet: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
let airdrop_signature = rpc_client.request_airdrop(&wallet.pubkey(), 2 * 1_000_000_000)?;
let recent_blockhash: solana_sdk::hash::Hash = rpc_client.get_latest_blockhash()?;
rpc_client.confirm_transaction_with_spinner(
&airdrop_signature,
&recent_blockhash,
CommitmentConfig::processed(),
)?;
println!("✅ - Step 1: Airdrop completed");
Ok(())
}
We are simply using the request_airdrop
function to request 2 SOL airdrop to the specified wallet. We wil use teh confirm_transaction_with_spinner
function to confirm the airdrop transaction with spinner effect in the terminal.
2. Wrap SOL
Next, let's create a function to wrap SOL:
fn wrap_sol(
rpc_client: &RpcClient,
wallet: &Keypair,
) -> Result<Pubkey, Box<dyn std::error::Error>> {
let associated_token_account =
get_associated_token_address(&wallet.pubkey(), &native_mint::id());
let instructions = vec![
spl_associated_token_account::instruction::create_associated_token_account(
&wallet.pubkey(),
&wallet.pubkey(),
&native_mint::id(),
&spl_token::id(),
),
system_instruction::transfer(&wallet.pubkey(), &associated_token_account, 1_000_000_000),
spl_instruction::sync_native(&spl_token::id(), &associated_token_account)?,
];
let recent_blockhash: solana_sdk::hash::Hash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&wallet.pubkey()),
&[wallet],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?;
println!("✅ - Step 2: SOL wrapped");
Ok(associated_token_account)
}
Let's walk through this function:
- First, we define an associated token account seeded on the wallet's public key and the native mint.
- Next, we assemble a transaction that has three instructions:
- We create the associated token account for wrapped SOL.
- We transfer 1 SOL to the newly created ATA.
- We call the
sync_native
instruction to update the amount field on the ATA to match the amount of wrapped SOL available.
- Finally, we send the transaction and log a success message.
3. Transfer Wrapped SOL
Now, let's create a function to transfer wrapped SOL:
fn transfer_wrapped_sol(
rpc_client: &RpcClient,
from_wallet: &Keypair,
to_wallet: &Keypair,
from_token_account: &Pubkey,
) -> Result<Pubkey, Box<dyn std::error::Error>> {
let to_token_account = get_associated_token_address(&to_wallet.pubkey(), &native_mint::id());
let instructions = vec![
spl_associated_token_account::instruction::create_associated_token_account(
&from_wallet.pubkey(),
&to_wallet.pubkey(),
&native_mint::id(),
&spl_token::id(),
),
spl_instruction::transfer(
&spl_token::id(),
from_token_account,
&to_token_account,
&from_wallet.pubkey(),
&[&from_wallet.pubkey()],
500_000_000,
)?,
];
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&from_wallet.pubkey()),
&[from_wallet],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?;
println!("✅ - Step 3: Transferred wrapped SOL");
Ok(to_token_account)
}
Since wrapped SOL is just an SPL token, this function is just a simple SPL token transfer. This function creates an ATA for the recipient (seeded on the native_mint
), then transfers half of the wrapped SOL to it.
4. Unwrap SOL
Let's add a function to unwrap SOL. To do this, all we need to do is close the associated token account and choose the desired address to send the token account's lamports.
fn unwrap_sol(
rpc_client: &RpcClient,
wallet: &Keypair,
token_account: &Pubkey,
) -> Result<(), Box<dyn std::error::Error>> {
let instruction = spl_instruction::close_account(
&spl_token::id(),
token_account,
&wallet.pubkey(),
&wallet.pubkey(),
&[&wallet.pubkey()],
)?;
let recent_blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&wallet.pubkey()),
&[wallet],
recent_blockhash,
);
rpc_client.send_and_confirm_transaction_with_spinner(&transaction)?;
println!("✅ - Step 4: SOL unwrapped");
Ok(())
}
This function closes the wrapped SOL associated token account, effectively unwrapping the SOL. We choose the destination for the lamports by passing in the wallet's public key.
5. Print Balances
Finally, let's add a function to print the balances:
fn print_balances(
rpc_client: &RpcClient,
wallet1: &Keypair,
wallet2: &Keypair,
token_account2: &Pubkey,
) -> Result<(), Box<dyn std::error::Error>> {
let wallet1_balance = rpc_client.get_balance(&wallet1.pubkey())?;
let wallet2_balance = rpc_client.get_balance(&wallet2.pubkey())?;
let token_account2_balance = rpc_client.get_token_account_balance(token_account2)?;
println!(
" - Wallet 1 SOL balance: {}",
wallet1_balance as f64 / 1_000_000_000.0
);
println!(
" - Wallet 2 SOL balance: {}",
wallet2_balance as f64 / 1_000_000_000.0
);
println!(
" - Wallet 2 wrapped SOL: {}",
token_account2_balance.ui_amount.unwrap()
);
Ok(())
}
This function fetches and displays the SOL and wrapped SOL balances for both wallets.
Run the Rust Script
To run the Rust script, make sure you have a local Solana validator running:
solana-test-validator -r
Then, in a separate terminal, navigate to your Rust project directory and run:
cargo run
You should see output similar to the JavaScript version, showing the steps of airdropping, wrapping, transferring, and unwrapping SOL, followed by the final balances:
cargo run
Compiling wrapped-sol-rust v0.1.0 (/wrapped-sol-rust)
Finished dev [unoptimized + debuginfo] target(s) in 2.45s
Running `target/debug/wrapped-sol-rust`
✅ - Step 1: Airdrop completed
✅ - Step 2: SOL wrapped
✅ - Step 3: Transferred wrapped SOL
✅ - Step 4: SOL unwrapped
- Wallet 1 SOL balance: 1.49794572
- Wallet 2 SOL balance: 0
- Wallet 2 wrapped SOL: 0.5
This Rust implementation provides the same functionality as the JavaScript and CLI versions, demonstrating how to interact with wrapped SOL using the Solana Rust SDK. Great work!
Wrap Up
This guide taught you how to interact with wrapped SOL on the Solana blockchain using Solana-Web3.js, Solana CLI, and the Solana Rust SDK. You created functions to airdrop SOL, wrap SOL, transfer wrapped SOL, unwrap SOL, and check balances. This knowledge forms a crucial foundation for developing more complex Solana applications involving SOL and SPL tokens.
Remember, while we used a local validator for this guide, you can modify the connection to work with devnet or mainnet for real-world applications. When you are ready to start interacting with devnet or mainnet, you will need an RPC endpoint. You can get one for free at QuickNode.com. Always exercise caution when working with real funds!
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.