Skip to main content

A Complete Guide to Wrapping, Transferring, and Unwrapping SOL on Solana

Updated on
Aug 21, 2024

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:

  1. Wrap SOL into wSOL
  2. Transfer wrapped SOL to another wallet
  3. Unwrap SOL

What You Will Need

Dependencies Used in this Guide

DependencyVersion
@solana/web3.js^1.91.1
@solana/spl-token^0.3.9
typescript^5.0.0
ts-node^10.9.1
solana cli1.18.8
spl-token cli>3.3.0
rust1.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:

  1. First, we define an associated token account seeded on the wallet's public key and the native mint.
  2. 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.
  3. 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:

  1. First, we define an associated token account seeded on the wallet's public key and the native mint.
  2. 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.
  3. 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.

Resources

Share this guide