Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions clients/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ solana-client = { workspace = true }
solana-commitment-config = { workspace = true }
solana-hash = { workspace = true }
solana-instruction = { workspace = true }
solana-keypair = { workspace = true }
solana-logger = { workspace = true }
solana-presigner = { workspace = true }
solana-program-pack = { workspace = true }
Expand Down
6 changes: 6 additions & 0 deletions clients/cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use {
crate::{
config::Config,
create_escrow_account::{command_create_escrow_account, CreateEscrowAccountArgs},
create_mint::{command_create_mint, CreateMintArgs},
find_pdas::{command_get_pdas, FindPdasArgs},
output::parse_output_format,
Expand Down Expand Up @@ -94,6 +95,8 @@ pub enum Command {
FindPdas(FindPdasArgs),
/// Convert wrapped tokens back into their original unwrapped version
Unwrap(UnwrapArgs),
/// Create an account used to escrow unwrapped tokens
CreateEscrowAccount(CreateEscrowAccountArgs),
}

impl Command {
Expand All @@ -108,6 +111,9 @@ impl Command {
Command::Wrap(args) => command_wrap(config, args, matches, wallet_manager).await,
Command::FindPdas(args) => command_get_pdas(config, args).await,
Command::Unwrap(args) => command_unwrap(config, args, matches, wallet_manager).await,
Command::CreateEscrowAccount(args) => {
command_create_escrow_account(config, args, matches, wallet_manager).await
}
}
}
}
30 changes: 29 additions & 1 deletion clients/cli/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use {
solana_pubkey::Pubkey,
solana_signature::Signature,
solana_transaction::Transaction,
spl_token_2022::{extension::PodStateWithExtensions, pod::PodAccount},
spl_token_2022::{
extension::{PodStateWithExtensions, StateWithExtensions},
pod::PodAccount,
state::Mint,
},
std::str::FromStr,
};

Expand Down Expand Up @@ -87,3 +91,27 @@ pub async fn get_account_owner(rpc_client: &RpcClient, account: &Pubkey) -> Resu
let owner = rpc_client.get_account(account).await?.owner;
Ok(owner)
}

pub async fn assert_mint_account(
rpc_client: &RpcClient,
account_key: &Pubkey,
) -> Result<(), String> {
let account_info = rpc_client
.get_account(account_key)
.await
.map_err(|e| format!("Failed to fetch account {}: {}", account_key, e))?;

let owner = account_info.owner;
if owner != spl_token::id() && owner != spl_token_2022::id() {
return Err(format!(
"Account {} is not owned by a token program. Owner: {}",
account_key, owner
));
}

// Attempt to deserialize the data as a mint account
let _ = StateWithExtensions::<Mint>::unpack(&account_info.data)
.map_err(|e| format!("Failed to unpack as spl token mint: {:?}", e))?;

Ok(())
}
262 changes: 262 additions & 0 deletions clients/cli/src/create_escrow_account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
use {
crate::{
common::{
assert_mint_account, get_account_owner, parse_pubkey, parse_token_program,
process_transaction,
},
config::Config,
output::{format_output, println_display},
CommandResult,
},
clap::Args,
serde_derive::{Deserialize, Serialize},
serde_with::{serde_as, DisplayFromStr},
solana_clap_v3_utils::{
input_parsers::signer::{SignerSource, SignerSourceParserBuilder},
keypair::signer_from_source,
},
solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay},
solana_program_pack::Pack,
solana_pubkey::Pubkey,
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_signature::Signature,
solana_signer::Signer,
solana_system_interface::instruction::create_account,
solana_transaction::Transaction,
spl_associated_token_account_client::{
address::get_associated_token_address_with_program_id,
instruction::create_associated_token_account,
},
spl_token_2022::instruction::initialize_account,
spl_token_wrap::{get_wrapped_mint_address, get_wrapped_mint_authority},
std::{
fmt::{Display, Formatter},
rc::Rc,
},
};

#[derive(Clone, Debug, Args)]
#[clap(about = "Creates an escrow token account for holding unwrapped tokens")]
pub struct CreateEscrowAccountArgs {
/// The address of the mint for the unwrapped tokens the escrow will hold
#[clap(value_parser = parse_pubkey)]
pub unwrapped_mint: Pubkey,

/// The address of the token program for the *wrapped* mint
#[clap(value_parser = parse_token_program)]
pub wrapped_token_program: Pubkey,

/// Keypair source for the escrow account itself.
/// If not provided, the Associated Token Account (`ATA`) for the wrapped
/// mint authority PDA will be used or created.
#[clap(long, value_parser = SignerSourceParserBuilder::default().allow_all().build())]
pub escrow_account_signer: Option<SignerSource>,

/// Do not error if the escrow account already exists and is initialized
#[clap(long)]
pub idempotent: bool,
}

#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateEscrowAccountOutput {
#[serde_as(as = "DisplayFromStr")]
pub escrow_account_address: Pubkey,

#[serde_as(as = "DisplayFromStr")]
pub escrow_account_owner: Pubkey, // This is the wrapped_mint_authority PDA

#[serde_as(as = "DisplayFromStr")]
pub unwrapped_token_program_id: Pubkey,

#[serde_as(as = "Option<DisplayFromStr>")]
pub signature: Option<Signature>,
}

impl Display for CreateEscrowAccountOutput {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln_name_value(
f,
"Escrow Account Address:",
&self.escrow_account_address.to_string(),
)?;
writeln_name_value(
f,
"Escrow Account Owner (wrapped mint authority):",
&self.escrow_account_owner.to_string(),
)?;
writeln_name_value(
f,
"Unwrapped Token Program ID:",
&self.unwrapped_token_program_id.to_string(),
)?;
if let Some(signature) = self.signature {
writeln_name_value(f, "Signature:", &signature.to_string())?;
}
Ok(())
}
}

impl QuietDisplay for CreateEscrowAccountOutput {
fn write_str(&self, _: &mut dyn std::fmt::Write) -> std::fmt::Result {
Ok(())
}
}
impl VerboseDisplay for CreateEscrowAccountOutput {}

pub async fn command_create_escrow_account(
config: &Config,
args: CreateEscrowAccountArgs,
matches: &clap::ArgMatches,
wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
) -> CommandResult {
let payer = config.fee_payer()?;
let rpc_client = config.rpc_client.clone();

// --- Validate Unwrapped Mint ---
assert_mint_account(&rpc_client, &args.unwrapped_mint).await?;

// --- Determine Unwrapped Token Program ---
let unwrapped_token_program_id = get_account_owner(&rpc_client, &args.unwrapped_mint).await?;

// --- Derive PDAs ---
let wrapped_mint_address =
get_wrapped_mint_address(&args.unwrapped_mint, &args.wrapped_token_program);
let wrapped_mint_authority = get_wrapped_mint_authority(&wrapped_mint_address);

println_display(
config,
format!(
"Creating escrow account under program {} for unwrapped mint {} owned by wrapped mint \
authority {}",
unwrapped_token_program_id, args.unwrapped_mint, wrapped_mint_authority
),
);

let mut instructions = Vec::new();
let mut signers = vec![payer.clone()];
let escrow_account_address: Pubkey;

// --- Decide How to Create Escrow Account ---
if let Some(signer_source) = &args.escrow_account_signer {
// --- Case 1: User Supplied a Signer for the Escrow Account ---
let escrow_signer = signer_from_source(
matches,
signer_source,
"escrow_account_signer",
wallet_manager,
)
.map_err(|e| format!("Failed to load escrow account signer: {}", e))?;
escrow_account_address = escrow_signer.pubkey();
signers.push(std::sync::Arc::from(escrow_signer));

// Check whether this account already exists.
match rpc_client.get_account(&escrow_account_address).await {
// Account exists
Ok(_) => {
if args.idempotent {
println_display(
config,
format!(
"Escrow account {} already exists, skipping creation",
escrow_account_address
),
);
} else {
return Err(format!(
"Escrow account {} already exists",
escrow_account_address
)
.into());
}
}
// Account does not exist, create it
Err(_) => {
let account_len = spl_token_2022::state::Account::LEN;
let rent_exempt_min = rpc_client
.get_minimum_balance_for_rent_exemption(account_len)
.await?;

instructions.push(create_account(
&payer.pubkey(),
&escrow_account_address,
rent_exempt_min,
account_len as u64,
&unwrapped_token_program_id,
));

instructions.push(initialize_account(
&unwrapped_token_program_id,
&escrow_account_address,
&args.unwrapped_mint,
&wrapped_mint_authority, // The PDA must be the owner
)?);
}
}
} else {
// --- Case 2: Default to Associated Token Account (ATA) ---
escrow_account_address = get_associated_token_address_with_program_id(
&wrapped_mint_authority,
&args.unwrapped_mint,
&unwrapped_token_program_id,
);

println_display(
config,
format!("Using ATA {} for escrow account", escrow_account_address),
);

match rpc_client.get_account(&escrow_account_address).await {
Ok(_) => {
if args.idempotent {
println_display(
config,
format!(
"Escrow account {} already exists, skipping creation",
escrow_account_address
),
);
} else {
return Err(format!(
"Escrow account {} already exists",
escrow_account_address
)
.into());
}
}
Err(_) => {
instructions.push(create_associated_token_account(
&payer.pubkey(),
&wrapped_mint_authority,
&args.unwrapped_mint,
&unwrapped_token_program_id,
));
}
}
}

// --- Build and Send Transaction if Needed ---
let signature = if instructions.is_empty() {
None
} else {
let latest_blockhash = rpc_client.get_latest_blockhash().await?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&payer.pubkey()),
&signers,
latest_blockhash,
);
process_transaction(config, transaction).await?
};

Ok(format_output(
config,
CreateEscrowAccountOutput {
escrow_account_address,
escrow_account_owner: wrapped_mint_authority,
unwrapped_token_program_id,
signature,
},
))
}
1 change: 1 addition & 0 deletions clients/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod cli;
mod common;
mod config;
mod create_escrow_account;
mod create_mint;
mod find_pdas;
mod output;
Expand Down
3 changes: 2 additions & 1 deletion clients/cli/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use {
solana_test_validator::{TestValidator, TestValidatorGenesis, UpgradeableProgramInfo},
solana_transaction::Transaction,
spl_associated_token_account_client::address::get_associated_token_address_with_program_id,
spl_token::{self, instruction::initialize_mint, state::Mint as SplTokenMint},
spl_token::{self, state::Mint as SplTokenMint},
spl_token_2022::instruction::initialize_mint,
std::{error::Error, path::PathBuf, process::Command, sync::Arc},
tempfile::NamedTempFile,
};
Expand Down
Loading