Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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" }
42 changes: 42 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,
UnwrapLamports,
}
impl fmt::Display for CommandName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -1721,6 +1722,47 @@ pub fn app<'a>(
.nonce_args(true)
.offline_args(),
)
.subcommand(
SubCommand::with_name(CommandName::UnwrapLamports.into())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this UnwrapSol so that people totally understand they should pass in SOL values

.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 tokens; 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(multisig_signer_arg())
.nonce_args(true)
.offline_args(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's include the allow_empty_recipient flag as in transfer, so people avoid sending lamports into a black hole accidentally

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this, which do you think would be better, i am considering if the recipient was unfunded then we first transfer the zero space rent exempt balance and then unwrap, or we check if the amount being unwrapped covers the rent exempt balance and if it doesn't then we transfer that first and then unwrap, if it does then we just unwrap, to prevent the transaction from failing due to the balance not being sufficient.

)
.subcommand(
SubCommand::with_name(CommandName::Approve.into())
.about("Approve a delegate for a token account")
Expand Down
103 changes: 103 additions & 0 deletions clients/cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,95 @@ 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>,
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 display_amount = amount
.map(|amount| amount.to_string())
.unwrap_or_else(|| "all".to_string());

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

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

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

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

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());
}
}

println_display(
config,
format!(
" Amount: {} SOL",
build_balance_message(account_data.lamports, false, false)
),
);
Comment on lines +2155 to +2161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to print this. It makes sense in the unwrap command because we're unwrapping that whole amount, but we've already printed how much we're going to unwrap earlier


// TODO: check if the destination account exists and if it doesn't check
// if the amount being transferred covers the rent exempt balance, then add
// a flag to fund the destination account
Comment on lines +2163 to +2165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid a flag to fund the destination account, just adding the allow-empty-recipient flag and accompanying check should be enough

}

println_display(config, format!(" Recipient: {}", &destination_account));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has already been printed earlier, so let's remove this print


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 +4362,20 @@ 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::UnwrapLamports, 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();

command_unwrap_lamports(config, amount, owner, source, 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
66 changes: 66 additions & 0 deletions clients/cli/tests/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,72 @@ async fn accounts_with_owner(test_validator: &TestValidator, payer: &Keypair) {
}

async fn wrapped_sol(test_validator: &TestValidator, payer: &Keypair) {
// both tests use the same ata so they can't run together
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the original wrapped sol tests and the unwrap lamport tests create and use the payer native mint ata, so one may end up failing depending on the order the tests are run, so i put them here so they run in order.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me!

unwrap_lamports(test_validator, payer).await;
wrap_unwrap_sol(test_validator, payer).await;
}

async fn unwrap_lamports(test_validator: &TestValidator, payer: &Keypair) {
let program_id = &spl_token_2022_interface::id();
let config = test_config_with_default_signer(test_validator, payer, program_id);
let native_mint = *Token::new_native(
config.program_client.clone(),
program_id,
config.fee_payer().unwrap().clone(),
)
.get_address();

process_test_command(
&config,
payer,
&["spl-token", CommandName::Wrap.into(), "10.0"],
)
.await
.unwrap();

let wrapped_account = get_associated_token_address_with_program_id(
&payer.pubkey(),
&native_mint,
&config.program_id,
);
let new_account = Keypair::new();

process_test_command(
&config,
payer,
&[
"spl-token",
CommandName::UnwrapLamports.into(),
"5.0",
&new_account.pubkey().to_string(),
"--from",
&wrapped_account.to_string(),
],
)
.await
.unwrap();
let balance = config
.rpc_client
.get_balance(&new_account.pubkey())
.await
.unwrap();
assert_eq!(balance, 5000000000);

process_test_command(
&config,
payer,
&["spl-token", CommandName::UnwrapLamports.into(), "ALL"],
)
.await
.unwrap();
config
.rpc_client
.get_account(&wrapped_account)
.await
.unwrap_err();
}

async fn wrap_unwrap_sol(test_validator: &TestValidator, payer: &Keypair) {
for program_id in VALID_TOKEN_PROGRAM_IDS.iter() {
let config = test_config_with_default_signer(test_validator, payer, program_id);
let native_mint = *Token::new_native(
Expand Down
1 change: 1 addition & 0 deletions clients/js-legacy/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from './thawAccount.js';
export * from './transfer.js';
export * from './transferChecked.js';
export * from './uiAmountToAmount.js';
export * from './unwrapLamports.js';
40 changes: 40 additions & 0 deletions clients/js-legacy/src/actions/unwrapLamports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
import { TOKEN_2022_PROGRAM_ID } from '../constants.js';
import { getSigners } from './internal.js';
import { createUnwrapLamportsInstruction } from '../instructions/unwrapLamports.js';

/**
* Unwrap lamports to an account
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Native source account
* @param destination Account receiving the lamports
* @param owner Owner of the source account
* @param amount Amount of lamports to unwrap
* @param multiSigners Signing accounts if `authority` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function unwrapLamports(
connection: Connection,
payer: Signer,
source: PublicKey,
destination: PublicKey,
owner: Signer | PublicKey,
amount: bigint | null,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_2022_PROGRAM_ID,
): Promise<TransactionSignature> {
const [ownerPublicKey, signers] = getSigners(owner, multiSigners);

const transaction = new Transaction().add(
createUnwrapLamportsInstruction(source, destination, ownerPublicKey, amount, multiSigners, programId),
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}
Loading