Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
44a6572
Add COption pack/unpack helpers
abelmarnk Nov 21, 2025
e2bba6f
Add instruction builder
abelmarnk Nov 21, 2025
fec6d0c
Add serde tests for new instruction using coption_u64
abelmarnk Nov 21, 2025
f9de3eb
Corrected instruction docs
abelmarnk Nov 26, 2025
ab7de00
Add PodTokenInstruction helpers
abelmarnk Nov 26, 2025
4e8c8bc
Add unwrap lamports processor
abelmarnk Nov 26, 2025
b242440
Add unwrap lamports cli clap_app parser
abelmarnk Nov 26, 2025
8613dfb
Add unwrap lamports cli handler
abelmarnk Nov 26, 2025
e53c1f9
Add auto-generated js code
abelmarnk Nov 26, 2025
2783e8f
Add js COption serialization helper
abelmarnk Nov 26, 2025
5a1672f
Add unwrap lamports js action & instruction
abelmarnk Nov 26, 2025
3f4e951
Add unwrap lamports js e2e tests
abelmarnk Nov 26, 2025
3a48e1d
Add unwrap lamports rust-legacy processor
abelmarnk Nov 26, 2025
ae8215b
Updated rust-legacy cpi guard helpers & tests
abelmarnk Nov 26, 2025
8a2d9aa
Added unwrap lamport rust-legacy tests
abelmarnk Nov 26, 2025
b756a7d
Remove spl-token-2022-interface patch
abelmarnk Nov 26, 2025
e815108
Remove memo code from unwrap lamports processor
abelmarnk Nov 26, 2025
1f22cb6
Fix self transfer in unwrap lamports processor
abelmarnk Nov 26, 2025
a290908
Refactor self transfer in unwrap lamports processor
abelmarnk Nov 26, 2025
be2db1c
Fix help description for UnwrapLamports amount argument
abelmarnk Nov 27, 2025
59b2905
Fix None lamport amount for UnwrapLamports
abelmarnk Nov 27, 2025
86eae0a
Remove CPI-guard delegate allowance check
abelmarnk Dec 2, 2025
f8be78c
Relax token program account check in instruction builder
abelmarnk Dec 2, 2025
3ede2b7
Racfactor and update processor for the self-transfer case
abelmarnk Dec 2, 2025
1e047fa
Add rust COptionU64 de-serialization helper
abelmarnk Dec 2, 2025
247f78d
Refactor and extend rust legacy tests
abelmarnk Dec 2, 2025
5be274d
Fix unwrap lamports js docs, imports & comments
abelmarnk Dec 2, 2025
2a06600
Fix instruction data check in unwrap lamports js instruction deconder
abelmarnk Dec 2, 2025
fe8dc70
Update js umwrap lamports test
abelmarnk Dec 2, 2025
4f355dd
Rename UnwrapLamports to UnwrapSol in cli parser
abelmarnk Dec 2, 2025
f4f1026
Add allow-unfunded-recipient flag
abelmarnk Dec 2, 2025
831ef27
Update cli unwrap lamports test & add unwrap lamports multisig test
abelmarnk Dec 2, 2025
210d576
Allow for unfunded recipients in unwrap lamports cli processor & upda…
abelmarnk Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,3 @@ consolidate-commits = false
[patch.crates-io]
spl-token-confidential-transfer-proof-extraction = { path = "confidential/proof-extraction" }
spl-token-confidential-transfer-proof-generation = { path = "confidential/proof-generation" }
spl-token-2022-interface = { path = "interface" }
48 changes: 48 additions & 0 deletions clients/cli/src/clap_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ pub enum CommandName {
UpdateUiAmountMultiplier,
Pause,
Resume,
UnwrapSol,
}
impl fmt::Display for CommandName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -1721,6 +1722,53 @@ pub fn app<'a>(
.nonce_args(true)
.offline_args(),
)
.subcommand(
SubCommand::with_name(CommandName::UnwrapSol.into())
.about("Unwrap lamports from a SOL token account")
.arg(
Arg::with_name("amount")
.value_parser(Amount::parse)
.value_name("TOKEN_AMOUNT")
.takes_value(true)
.index(1)
.required(true)
.help("Amount to unwrap, in SOL; accepts keyword ALL"),
)
.arg(
Arg::with_name("recipient")
.validator(|s| is_valid_pubkey(s))
.value_name("RECIPIENT_ACCOUNT_ADDRESS")
.takes_value(true)
.index(2)
.help("Specify the address to recieve the unwrapped SOL. \
Defaults to the owner address.")
)
.arg(
Arg::with_name("from")
.validator(|s| is_valid_pubkey(s))
.value_name("NATIVE_TOKEN_ACCOUNT_ADDRESS")
.takes_value(true)
.long("from")
.help("Specify the token account that contains the wrapped SOL. \
[default: owner's associated token account]")
)
.arg(owner_keypair_arg_with_value_name("NATIVE_TOKEN_OWNER_KEYPAIR")
.help(
"Specify the keypair for the wallet which owns the wrapped SOL. \
This may be a keypair file or the ASK keyword. \
Defaults to the client keypair.",
),
)
.arg(
Arg::with_name("allow_unfunded_recipient")
.long("allow-unfunded-recipient")
.takes_value(false)
.help("Complete the transfer even if the recipient address is not funded")
)
.arg(multisig_signer_arg())
.nonce_args(true)
.offline_args(),
)
.subcommand(
SubCommand::with_name(CommandName::Approve.into())
.about("Approve a delegate for a token account")
Expand Down
152 changes: 151 additions & 1 deletion clients/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ use {
program_option::COption,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
},
solana_system_interface::program as system_program,
solana_system_interface::{instruction::transfer, program as system_program},
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id,
spl_pod::optional_keys::OptionalNonZeroPubkey,
spl_token_2022::extension::confidential_transfer::account_info::{
Expand Down Expand Up @@ -2099,6 +2100,130 @@ async fn command_unwrap(
})
}

async fn command_unwrap_lamports(
config: &Config<'_>,
ui_amount: Amount,
source_owner: Pubkey,
source_account: Option<Pubkey>,
destination_account: Option<Pubkey>,
allow_unfunded_recipient: bool,
bulk_signers: BulkSigners,
) -> CommandResult {
let use_associated_account = source_account.is_none();
let token = native_token_client_from_config(config)?;

let source_account =
source_account.unwrap_or_else(|| token.get_associated_token_address(&source_owner));

let destination_account = destination_account.unwrap_or(source_owner);

let amount = match ui_amount.sol_to_lamport() {
Amount::Raw(ui_amount) => Some(ui_amount),
Amount::Decimal(_) => unreachable!(),
Amount::All => None,
};
let mut balance = None;

if !config.sign_only {
let account_data = config.get_account_checked(&source_account).await?;

if account_data.lamports == 0 {
if use_associated_account {
return Err("No wrapped SOL in associated account; did you mean to specify an auxiliary address?".to_string().into());
} else {
return Err(format!("No wrapped SOL in {}", source_account).into());
}
}

let account_state = StateWithExtensionsOwned::<Account>::unpack(account_data.data)?;

if let Some(amount) = amount {
if account_state.base.amount < amount {
return Err(format!(
"Error: Sender has insufficient funds, current balance is {} SOL",
build_balance_message(account_state.base.amount, false, false)
)
.into());
}
}

balance = Some(account_state.base.amount);

if !use_associated_account && account_state.base.mint != *token.get_address() {
return Err(format!("{} is not a native token account", source_account).into());
}

if config.rpc_client.get_balance(&destination_account).await? == 0 {
// if it doesn't exist, we gate transfer with a different flag
if allow_unfunded_recipient {
println_display(
config,
format!("Funding recipient: {}", destination_account,),
);

let rent_exempt_lamports = config
.rpc_client
.get_minimum_balance_for_rent_exemption(0)
.await?;
let fee_payer = config.fee_payer()?;
let instruction = transfer(
&fee_payer.pubkey(),
&destination_account,
rent_exempt_lamports,
);
let recent_blockhash = config.rpc_client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&fee_payer.pubkey()),
&[fee_payer],
recent_blockhash,
);
config
.rpc_client
.send_and_confirm_transaction(&transaction)
.await?;
} else {
return Err("Error: The recipient address is not funded. \
Add `--allow-unfunded-recipient` to complete the transfer."
.into());
}
}
}

let display_amount = amount
.or(balance)
.map(|amount| build_balance_message(amount, false, false))
.unwrap_or_else(|| "all".to_string());

println_display(
config,
format!(
"Unwrapping {} SOL to {}",
display_amount, destination_account
),
);

let res = token
.unwrap_lamports(
&source_account,
&destination_account,
&source_owner,
amount,
&bulk_signers,
)
.await?;

let tx_return = finish_tx(config, &res, false).await?;
Ok(match tx_return {
TransactionReturnData::CliSignature(signature) => {
config.output_format.formatted_string(&signature)
}
TransactionReturnData::CliSignOnlyData(sign_only_data) => {
config.output_format.formatted_string(&sign_only_data)
}
})
}

#[allow(clippy::too_many_arguments)]
async fn command_approve(
config: &Config<'_>,
Expand Down Expand Up @@ -4273,6 +4398,31 @@ pub async fn process_command(
let account = pubkey_of_signer(arg_matches, "account", &mut wallet_manager).unwrap();
command_unwrap(config, wallet_address, account, bulk_signers).await
}
(CommandName::UnwrapSol, arg_matches) => {
let (owner_signer, owner) =
config.signer_or_default(arg_matches, "owner", &mut wallet_manager);
if config.multisigner_pubkeys.is_empty() {
push_signer_with_dedup(owner_signer, &mut bulk_signers);
}

let amount = *arg_matches.get_one::<Amount>("amount").unwrap();
let source = pubkey_of_signer(arg_matches, "from", &mut wallet_manager).unwrap();
let recipient =
pubkey_of_signer(arg_matches, "recipient", &mut wallet_manager).unwrap();

let allow_unfunded_recipient = arg_matches.is_present("allow_unfunded_recipient");

command_unwrap_lamports(
config,
amount,
owner,
source,
recipient,
allow_unfunded_recipient,
bulk_signers,
)
.await
}
(CommandName::Approve, arg_matches) => {
let (owner_signer, owner_address) =
config.signer_or_default(arg_matches, "owner", &mut wallet_manager);
Expand Down
Loading
Loading