Skip to content
220 changes: 218 additions & 2 deletions solana-programs/claimable-tokens/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ use std::ops::Mul;
use anyhow::anyhow;
use anyhow::{bail, Context};
use borsh::BorshSerialize;
use claimable_tokens::state::{NonceAccount, TransferInstructionData};
use claimable_tokens::state::{NonceAccount, TransferInstructionData, SignedSetAuthorityData};
use claimable_tokens::utils::program::{find_nonce_address, NONCE_ACCOUNT_PREFIX};
use claimable_tokens::{
instruction::CreateTokenAccount,
utils::program::{find_address_pair, EthereumAddress},
};
use claimable_tokens::instruction::{
CreateTokenAccount,
};
use clap::{
crate_description, crate_name, crate_version, value_t, App, AppSettings, Arg, ArgMatches,
SubCommand,
};
use solana_clap_utils::input_parsers::keypair_of;
use solana_clap_utils::input_validators::is_keypair;
use solana_clap_utils::{
fee_payer::fee_payer_arg,
input_parsers::pubkey_of,
Expand All @@ -22,6 +26,9 @@ use solana_clap_utils::{
};
use solana_client::{rpc_client::RpcClient, rpc_response::Response};
use solana_program::instruction::Instruction;
use solana_program::program_option::COption;
use solana_sdk::signature::Keypair;
use solana_sdk::{secp256k1_instruction, bs58};
use solana_sdk::{
account::ReadableAccount,
commitment_config::CommitmentConfig,
Expand Down Expand Up @@ -203,6 +210,7 @@ fn send_to(config: Config, eth_address: [u8; 20], mint: Pubkey, amount: f64) ->
// Checking if the derived address of recipient does not exist
// then we must add instruction to create it
let derived_token_acc_data = config.rpc_client.get_account_data(&pair.derive.address);

if derived_token_acc_data.is_err() {
instructions.push(claimable_tokens::instruction::init(
&claimable_tokens::id(),
Expand Down Expand Up @@ -265,6 +273,107 @@ fn balance(config: Config, eth_address: EthereumAddress, mint: Pubkey) -> anyhow
Ok(())
}

fn init_account(
config: Config,
eth_address: EthereumAddress,
mint: Pubkey,
) -> anyhow::Result<()> {
let instruction = claimable_tokens::instruction::init(
&claimable_tokens::id(),
&config.fee_payer.pubkey(),
&mint,
CreateTokenAccount { eth_address },
)?;
let mut tx =
Transaction::new_with_payer(&[instruction], Some(&config.fee_payer.pubkey()));
let (recent_blockhash, _) = config.rpc_client.get_recent_blockhash()?;
tx.sign(&[config.fee_payer.as_ref()], recent_blockhash);
let tx_hash = config
.rpc_client
.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.rpc_client.commitment(),
solana_client::rpc_config::RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: None,
encoding: None,
max_retries: None,
},
)?;
println!("Init completed, transaction hash: {:?}", tx_hash);
Ok(())
}

fn set_authority(
config: Config,
eth_private_key: libsecp256k1::SecretKey,
mint: Pubkey,
new_authority: Pubkey,
authority_type: spl_token::instruction::AuthorityType,
) -> anyhow::Result<()> {
let eth_pubkey = libsecp256k1::PublicKey::from_secret_key(&eth_private_key);
let eth_address = construct_eth_pubkey(&eth_pubkey);
let pair = find_address_pair(&claimable_tokens::id(), &mint, eth_address)?;

let (recent_blockhash, _) = config.rpc_client.get_recent_blockhash()?;

let signed_instruction = spl_token::instruction::TokenInstruction::SetAuthority {
authority_type,
new_authority: COption::Some(new_authority),
};

let message = SignedSetAuthorityData {
blockhash: recent_blockhash,
instruction: signed_instruction.pack(),
};

let signature_instr = secp256k1_instruction::new_secp256k1_instruction(
&eth_private_key,
&message.try_to_vec().unwrap(),
);
let set_authority_instruction = claimable_tokens::instruction::set_authority(
&claimable_tokens::id(),
&pair.derive.address,
&pair.base.address,
)?;
let mut tx =
Transaction::new_with_payer(&[signature_instr, set_authority_instruction], Some(&config.fee_payer.pubkey()));
tx.sign(
&[config.fee_payer.as_ref(), config.owner.as_ref()],
recent_blockhash,
);
let tx_hash = config
.rpc_client
.send_and_confirm_transaction_with_spinner(&tx)?;
println!("Set authority completed, transaction hash: {:?}", tx_hash);
Ok(())
}

fn close(
config: Config,
eth_address: EthereumAddress,
mint: Pubkey,
destination_account: Pubkey,
) -> anyhow::Result<()> {
let pair = find_address_pair(&claimable_tokens::id(), &mint, eth_address)?;
let instruction = claimable_tokens::instruction::close(
&claimable_tokens::id(),
&pair.derive.address,
&pair.base.address,
&destination_account,
eth_address
)?;
let mut tx =
Transaction::new_with_payer(&[instruction], Some(&config.fee_payer.pubkey()));
let (recent_blockhash, _) = config.rpc_client.get_recent_blockhash()?;
tx.sign(&[config.fee_payer.as_ref()], recent_blockhash);
let tx_hash = config
.rpc_client
.send_and_confirm_transaction_with_spinner(&tx)?;
println!("Close completed, transaction hash: {:?}", tx_hash);
Ok(())
}

fn main() -> anyhow::Result<()> {
let matches = App::new(crate_name!())
.about(crate_description!())
Expand Down Expand Up @@ -383,6 +492,65 @@ fn main() -> anyhow::Result<()> {
.help("Program Id address"),
])
.help("Receives balance of account that associated with Ethereum address and specific mint."),
SubCommand::with_name("init").args(&[
Arg::with_name("eth_address")
.value_name("ETHEREUM_ADDRESS")
.takes_value(true)
.required(true)
.help("Ethereum address to create token account for"),
Arg::with_name("mint")
.validator(is_pubkey)
.value_name("MINT_ADDRESS")
.takes_value(true)
.required(true)
.help("Token mint address"),
])
.help("Create a token account for the specified Ethereum address and mint."),
SubCommand::with_name("set-authority").args(&[
Arg::with_name("private_key")
.value_name("ETHEREUM_PRIVATE_KEY")
.takes_value(true)
.required(true)
.help("Ethereum private key associated with the token account to change authority of."),
Arg::with_name("mint")
.validator(is_pubkey)
.value_name("MINT_ADDRESS")
.takes_value(true)
.required(true)
.help("Token mint address"),
Arg::with_name("new_authority")
.validator(is_pubkey)
.value_name("NEW_AUTHORITY")
.takes_value(true)
.required(false)
.help("New authority to set for the token account."),
Arg::with_name("authority_type")
.value_name("AUTHORITY_TYPE")
.takes_value(true)
.required(false)
.help("Type of authority to set for the token account."),
])
.help("Set a new authority for the token account associated with the specified Ethereum address and mint."),
SubCommand::with_name("close").args(&[
Arg::with_name("eth_address")
.value_name("ETHEREUM_ADDRESS")
.takes_value(true)
.required(true)
.help("Ethereum address to close token account for"),
Arg::with_name("mint")
.validator(is_pubkey)
.value_name("MINT_ADDRESS")
.takes_value(true)
.required(true)
.help("Token mint address"),
Arg::with_name("rent_destination")
.validator(is_pubkey)
.value_name("RENT_DESTINATION")
.takes_value(true)
.required(true)
.help("Where to return the rent to when the account is closed."),
])
.help("Close the token account associated with the specified Ethereum address and mint."),
])
.get_matches();

Expand Down Expand Up @@ -478,6 +646,54 @@ fn main() -> anyhow::Result<()> {
})()
.context("Preparing parameters for execution command `send to`")?;
}
("init", Some(args)) => {
let (mint, eth_address) = (|| -> anyhow::Result<_> {
let eth_address = eth_address_of(args, "eth_address")?;
let mint = pubkey_of(args, "mint").unwrap();

Ok((mint, eth_address))
})()
.context("Preparing parameters for execution command `init`")?;

init_account(config, eth_address, mint)
.context("Failed to execute `init` command")?
}
("set-authority", Some(args)) => {
let (eth_private_key, mint, new_authority, authority_type) = (|| -> anyhow::Result<_> {
let eth_private_key = eth_seckey_of(args, "private_key")?;
let mint = pubkey_of(args, "mint").unwrap();
let new_authority = pubkey_of(args, "new_authority").unwrap();
let authority_type_arg = args.value_of("authority_type").unwrap();
if authority_type_arg != "AccountOwner" && authority_type_arg != "CloseAccount" {
bail!("Invalid authority type provided. Must be either 'AccountOwner' or 'CloseAccount'");
}
let authority_type = match authority_type_arg {
"AccountOwner" => spl_token::instruction::AuthorityType::AccountOwner,
"CloseAccount" => spl_token::instruction::AuthorityType::CloseAccount,
_ => unreachable!(),
};

Ok((eth_private_key, mint, new_authority, authority_type))
})()
.context("Preparing parameters for execution command `set-authority`")?;

set_authority(config, eth_private_key, mint, new_authority, authority_type)
.context("Failed to execute `set-authority` command")?
}
("close", Some(args)) => {
let (eth_address, mint, rent_destination) = (|| -> anyhow::Result<_> {
let eth_address = eth_address_of(args, "eth_address")?;
let mint = pubkey_of(args, "mint").unwrap();
let rent_destination = pubkey_of(args, "rent_destination").unwrap();

Ok((eth_address, mint, rent_destination))
})()
.context("Preparing parameters for execution command `close`")?;

close(config, eth_address, mint, rent_destination)
.context("Failed to execute `close` command")?
}

_ => unreachable!(),
}
Ok(())
Expand Down
3 changes: 3 additions & 0 deletions solana-programs/claimable-tokens/program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum ClaimableProgramError {
/// User nonce verification error
#[error("Nonce verification failed")]
NonceVerificationError,
/// Invalid signature data
#[error("Invalid signature data")]
InvalidSignatureData,
}
impl From<ClaimableProgramError> for ProgramError {
fn from(e: ClaimableProgramError) -> Self {
Expand Down
67 changes: 67 additions & 0 deletions solana-programs/claimable-tokens/program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ pub enum ClaimableProgramInstruction {
/// 7. `[r]` System program id
/// 8. `[r]` SPL token account id
Transfer(EthereumAddress),

/// Set authority
///
/// 0. `[w]` Token acc to change owner of (bank account)
/// 1. `[r]` Banks token account authority (current owner)
/// 2. `[r]` Sysvar instruction id
/// 3. `[r]` Sysvar recent blockhashes id
/// 4. `[r]` SPL token account id
SetAuthority,

/// Close token account
///
/// 0. `[w]` Token acc to close
/// 1. `[r]` Token acc authority
/// 2. `[w]` Destination acc to receive rent
/// 3. `[r]` SPL token account id
/// 4. `[s]` Close authority (if different from token acc authority)
Close(EthereumAddress),
}

/// Create `CreateTokenAccount` instruction
Expand Down Expand Up @@ -104,3 +122,52 @@ pub fn transfer(
data,
})
}

/// Create `TransferOwnership` instruction
///
/// NOTE: Instruction must followed after `new_secp256k1_instruction`
/// with params: current owner ethereum private key and bank token account public key.
/// Otherwise error message `Secp256 instruction losing` will be issued
pub fn set_authority(
program_id: &Pubkey,
banks_token_acc: &Pubkey,
authority: &Pubkey,
) -> Result<Instruction, ProgramError> {
let data = ClaimableProgramInstruction::SetAuthority.try_to_vec()?;
let accounts = vec![
AccountMeta::new(*banks_token_acc, false),
AccountMeta::new_readonly(*authority, false),
AccountMeta::new_readonly(sysvar::instructions::id(), false),
AccountMeta::new_readonly(sysvar::recent_blockhashes::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
];
Ok(Instruction {
program_id: *program_id,
accounts,
data,
})
}

/// Create `Close` instruction
pub fn close(
program_id: &Pubkey,
token_account: &Pubkey,
authority: &Pubkey,
destination_account: &Pubkey,
eth_address: EthereumAddress,
) -> Result<Instruction, ProgramError> {
let data = ClaimableProgramInstruction::Close(eth_address)
.try_to_vec()
.unwrap();
let mut accounts = vec![
AccountMeta::new(*token_account, false),
AccountMeta::new_readonly(*authority, false),
AccountMeta::new(*destination_account, false),
AccountMeta::new_readonly(spl_token::id(), false),
];
Ok(Instruction {
program_id: *program_id,
accounts,
data,
})
}
Loading