-
Notifications
You must be signed in to change notification settings - Fork 131
Add unwrap_lamports instruction
#857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 16 commits
48cf9eb
f9e2ca7
58d0323
d374c8e
993d722
98ec326
011157d
eb71194
6b4de83
b8119a8
b401e17
20d2610
9eacd3f
721a16a
6488ab5
0d937fe
9a773e6
ed37839
f7bf0da
bfcf620
93c42fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -1721,6 +1722,47 @@ pub fn app<'a>( | |
| .nonce_args(true) | ||
| .offline_args(), | ||
| ) | ||
| .subcommand( | ||
| SubCommand::with_name(CommandName::UnwrapLamports.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 tokens; accepts keyword ALL"), | ||
abelmarnk marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
| .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(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's include the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| // 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
|
|
||
| println_display(config, format!(" Recipient: {}", &destination_account)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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<'_>, | ||
|
|
@@ -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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
|
||
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's call this
UnwrapSolso that people totally understand they should pass in SOL values