Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit f80b00b

Browse files
authored
token-cli: Fix offline signing by making simulated compute unit limit explicit (#6550)
* Improve offline test to actually send the transaction * token-client: Specify compute unit limit more clearly * Require compute unit limit if price is set * Parse args as suggested * Test using compute unit price for offline signing * Fixup test * Set static limit properly for set_interest_rate * Use clap error directly
1 parent 0c50b78 commit f80b00b

File tree

7 files changed

+198
-85
lines changed

7 files changed

+198
-85
lines changed

token/cli/src/clap_app.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use {
1717
spl_token_2022::instruction::{AuthorityType, MAX_SIGNERS, MIN_SIGNERS},
1818
std::{fmt, str::FromStr},
1919
strum::IntoEnumIterator,
20-
strum_macros::{EnumIter, EnumString, IntoStaticStr},
20+
strum_macros::{AsRefStr, EnumIter, EnumString, IntoStaticStr},
2121
};
2222

2323
pub type Error = Box<dyn std::error::Error + Send + Sync>;
@@ -78,7 +78,7 @@ pub const COMPUTE_UNIT_LIMIT_ARG: ArgConstant<'static> = ArgConstant {
7878

7979
pub static VALID_TOKEN_PROGRAM_IDS: [Pubkey; 2] = [spl_token_2022::ID, spl_token::ID];
8080

81-
#[derive(Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)]
81+
#[derive(AsRefStr, Debug, Clone, Copy, PartialEq, EnumString, IntoStaticStr)]
8282
#[strum(serialize_all = "kebab-case")]
8383
pub enum CommandName {
8484
CreateToken,
@@ -350,6 +350,7 @@ where
350350
Err(e) => Err(e),
351351
}
352352
}
353+
353354
struct SignOnlyNeedsFullMintSpec {}
354355
impl offline::ArgsConfig for SignOnlyNeedsFullMintSpec {
355356
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {

token/cli/src/command.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use {
6868
},
6969
spl_token_client::{
7070
client::{ProgramRpcClientSendTransaction, RpcClientResponse},
71-
token::{ExtensionInitializationParams, Token},
71+
token::{ComputeUnitLimit, ExtensionInitializationParams, Token},
7272
},
7373
spl_token_group_interface::state::TokenGroup,
7474
spl_token_metadata_interface::state::{Field, TokenMetadata},
@@ -152,11 +152,7 @@ fn config_token_client(
152152
token: Token<ProgramRpcClientSendTransaction>,
153153
config: &Config<'_>,
154154
) -> Result<Token<ProgramRpcClientSendTransaction>, Error> {
155-
let token = if let Some(compute_unit_limit) = config.compute_unit_limit {
156-
token.with_compute_unit_limit(compute_unit_limit)
157-
} else {
158-
token
159-
};
155+
let token = token.with_compute_unit_limit(config.compute_unit_limit.clone());
160156

161157
let token = if let Some(compute_unit_price) = config.compute_unit_price {
162158
token.with_compute_unit_price(compute_unit_price)
@@ -432,10 +428,13 @@ async fn command_set_interest_rate(
432428
rate_bps: i16,
433429
bulk_signers: Vec<Arc<dyn Signer>>,
434430
) -> CommandResult {
431+
let mut token = token_client_from_config(config, &token_pubkey, None)?;
435432
// Because set_interest_rate depends on the time, it can cost more between
436433
// simulation and execution. To help that, just set a static compute limit
437-
let token = base_token_client(config, &token_pubkey, None)?.with_compute_unit_limit(2_500);
438-
let token = config_token_client(token, config)?;
434+
// if none has been set
435+
if !matches!(config.compute_unit_limit, ComputeUnitLimit::Static(_)) {
436+
token = token.with_compute_unit_limit(ComputeUnitLimit::Static(2_500));
437+
}
439438

440439
if !config.sign_only {
441440
let mint_account = config.get_account_checked(&token_pubkey).await?;

token/cli/src/config.rs

+30-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ use {
2020
extension::StateWithExtensionsOwned,
2121
state::{Account, Mint},
2222
},
23-
spl_token_client::client::{
24-
ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction,
23+
spl_token_client::{
24+
client::{
25+
ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction,
26+
},
27+
token::ComputeUnitLimit,
2528
},
2629
std::{process::exit, rc::Rc, sync::Arc},
2730
};
@@ -68,7 +71,7 @@ pub struct Config<'a> {
6871
pub program_id: Pubkey,
6972
pub restrict_to_program_id: bool,
7073
pub compute_unit_price: Option<u64>,
71-
pub compute_unit_limit: Option<u32>,
74+
pub compute_unit_limit: ComputeUnitLimit,
7275
}
7376

7477
impl<'a> Config<'a> {
@@ -280,9 +283,32 @@ impl<'a> Config<'a> {
280283
(default_program_id, false)
281284
};
282285

286+
// need to specify a compute limit if compute price and blockhash are specified
287+
if matches.is_present(BLOCKHASH_ARG.name)
288+
&& matches.is_present(COMPUTE_UNIT_PRICE_ARG.name)
289+
&& !matches.is_present(COMPUTE_UNIT_LIMIT_ARG.name)
290+
{
291+
clap::Error::with_description(
292+
&format!(
293+
"Need to set `{}` if `{}` and `--{}` are set",
294+
COMPUTE_UNIT_LIMIT_ARG.long, COMPUTE_UNIT_PRICE_ARG.long, BLOCKHASH_ARG.long,
295+
),
296+
clap::ErrorKind::MissingRequiredArgument,
297+
)
298+
.exit();
299+
}
300+
283301
let nonce_blockhash = value_of(matches, BLOCKHASH_ARG.name);
284302
let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
285-
let compute_unit_limit = value_of(matches, COMPUTE_UNIT_LIMIT_ARG.name);
303+
let compute_unit_limit = value_of(matches, COMPUTE_UNIT_LIMIT_ARG.name)
304+
.map(ComputeUnitLimit::Static)
305+
.unwrap_or_else(|| {
306+
if nonce_blockhash.is_some() {
307+
ComputeUnitLimit::Default
308+
} else {
309+
ComputeUnitLimit::Simulated
310+
}
311+
});
286312
Self {
287313
default_signer,
288314
rpc_client,

token/cli/tests/command.rs

+116-40
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ use {
4444
client::{
4545
ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction,
4646
},
47-
token::Token,
47+
token::{ComputeUnitLimit, Token},
4848
},
4949
spl_token_group_interface::state::{TokenGroup, TokenGroupMember},
5050
spl_token_metadata_interface::state::TokenMetadata,
51-
std::{ffi::OsString, path::PathBuf, str::FromStr, sync::Arc},
51+
std::{
52+
ffi::{OsStr, OsString},
53+
path::PathBuf,
54+
str::FromStr,
55+
sync::Arc,
56+
},
5257
tempfile::NamedTempFile,
5358
};
5459

@@ -201,7 +206,7 @@ fn test_config_with_default_signer<'a>(
201206
program_id: *program_id,
202207
restrict_to_program_id: true,
203208
compute_unit_price: None,
204-
compute_unit_limit: None,
209+
compute_unit_limit: ComputeUnitLimit::Simulated,
205210
}
206211
}
207212

@@ -230,7 +235,7 @@ fn test_config_without_default_signer<'a>(
230235
program_id: *program_id,
231236
restrict_to_program_id: true,
232237
compute_unit_price: None,
233-
compute_unit_limit: None,
238+
compute_unit_limit: ComputeUnitLimit::Simulated,
234239
}
235240
}
236241

@@ -441,7 +446,7 @@ where
441446
process_command(&sub_command, matches, config, wallet_manager, bulk_signers).await
442447
}
443448

444-
async fn exec_test_cmd(config: &Config<'_>, args: &[&str]) -> CommandResult {
449+
async fn exec_test_cmd<T: AsRef<OsStr>>(config: &Config<'_>, args: &[T]) -> CommandResult {
445450
let default_decimals = format!("{}", spl_token_2022::native_mint::DECIMALS);
446451
let minimum_signers_help = minimum_signers_help_string();
447452
let multisig_member_help = multisig_member_help_string();
@@ -2974,10 +2979,17 @@ async fn multisig_transfer(test_validator: &TestValidator, payer: &Keypair) {
29742979
}
29752980
}
29762981

2977-
async fn offline_multisig_transfer_with_nonce(test_validator: &TestValidator, payer: &Keypair) {
2982+
async fn do_offline_multisig_transfer(
2983+
test_validator: &TestValidator,
2984+
payer: &Keypair,
2985+
compute_unit_price: Option<u64>,
2986+
) {
29782987
let m = 2;
29792988
let n = 3u8;
29802989

2990+
let fee_payer_keypair_file = NamedTempFile::new().unwrap();
2991+
write_keypair_file(payer, &fee_payer_keypair_file).unwrap();
2992+
29812993
let (multisig_members, multisig_paths): (Vec<_>, Vec<_>) = std::iter::repeat_with(Keypair::new)
29822994
.take(n as usize)
29832995
.map(|s| {
@@ -2988,6 +3000,7 @@ async fn offline_multisig_transfer_with_nonce(test_validator: &TestValidator, pa
29883000
.unzip();
29893001
for program_id in VALID_TOKEN_PROGRAM_IDS.iter() {
29903002
let mut config = test_config_with_default_signer(test_validator, payer, program_id);
3003+
config.compute_unit_limit = ComputeUnitLimit::Default;
29913004
let token = create_token(&config, payer).await;
29923005
let nonce = create_nonce(&config, payer).await;
29933006

@@ -3032,58 +3045,121 @@ async fn offline_multisig_transfer_with_nonce(test_validator: &TestValidator, pa
30323045
let program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>> = Arc::new(
30333046
ProgramOfflineClient::new(blockhash, ProgramRpcClientSendTransaction),
30343047
);
3048+
let mut args = vec![
3049+
"spl-token".to_string(),
3050+
CommandName::Transfer.as_ref().to_string(),
3051+
token.to_string(),
3052+
"10".to_string(),
3053+
destination.to_string(),
3054+
"--blockhash".to_string(),
3055+
blockhash.to_string(),
3056+
"--nonce".to_string(),
3057+
nonce.to_string(),
3058+
"--nonce-authority".to_string(),
3059+
payer.pubkey().to_string(),
3060+
"--sign-only".to_string(),
3061+
"--mint-decimals".to_string(),
3062+
format!("{}", TEST_DECIMALS),
3063+
"--multisig-signer".to_string(),
3064+
multisig_paths[1].path().to_str().unwrap().to_string(),
3065+
"--multisig-signer".to_string(),
3066+
multisig_members[2].to_string(),
3067+
"--from".to_string(),
3068+
source.to_string(),
3069+
"--owner".to_string(),
3070+
multisig_pubkey.to_string(),
3071+
"--fee-payer".to_string(),
3072+
payer.pubkey().to_string(),
3073+
"--program-id".to_string(),
3074+
program_id.to_string(),
3075+
];
3076+
if let Some(compute_unit_price) = compute_unit_price {
3077+
args.push("--with-compute-unit-price".to_string());
3078+
args.push(compute_unit_price.to_string());
3079+
args.push("--with-compute-unit-limit".to_string());
3080+
args.push(10_000.to_string());
3081+
}
30353082
config.program_client = program_client;
3036-
let result = exec_test_cmd(
3037-
&config,
3038-
&[
3039-
"spl-token",
3040-
CommandName::Transfer.into(),
3041-
&token.to_string(),
3042-
"10",
3043-
&destination.to_string(),
3044-
"--blockhash",
3045-
&blockhash.to_string(),
3046-
"--nonce",
3047-
&nonce.to_string(),
3048-
"--nonce-authority",
3049-
&payer.pubkey().to_string(),
3050-
"--sign-only",
3051-
"--mint-decimals",
3052-
&format!("{}", TEST_DECIMALS),
3053-
"--multisig-signer",
3054-
multisig_paths[1].path().to_str().unwrap(),
3055-
"--multisig-signer",
3056-
&multisig_members[2].to_string(),
3057-
"--from",
3058-
&source.to_string(),
3059-
"--owner",
3060-
&multisig_pubkey.to_string(),
3061-
"--fee-payer",
3062-
&multisig_members[0].to_string(),
3063-
],
3064-
)
3065-
.await
3066-
.unwrap();
3083+
let result = exec_test_cmd(&config, &args).await.unwrap();
30673084
// the provided signer has a signature, denoted by the pubkey followed
30683085
// by "=" and the signature
3069-
assert!(result.contains(&format!("{}=", multisig_members[1])));
3086+
let member_prefix = format!("{}=", multisig_members[1]);
3087+
let signature_position = result.find(&member_prefix).unwrap();
3088+
let end_position = result[signature_position..].find('\n').unwrap();
3089+
let signer = result[signature_position..].get(..end_position).unwrap();
30703090

30713091
// other three expected signers are absent
30723092
let absent_signers_position = result.find("Absent Signers").unwrap();
30733093
let absent_signers = result.get(absent_signers_position..).unwrap();
3074-
assert!(absent_signers.contains(&multisig_members[0].to_string()));
30753094
assert!(absent_signers.contains(&multisig_members[2].to_string()));
30763095
assert!(absent_signers.contains(&payer.pubkey().to_string()));
30773096

30783097
// and nothing else is marked a signer
3098+
assert!(!absent_signers.contains(&multisig_members[0].to_string()));
30793099
assert!(!absent_signers.contains(&multisig_pubkey.to_string()));
30803100
assert!(!absent_signers.contains(&nonce.to_string()));
30813101
assert!(!absent_signers.contains(&source.to_string()));
30823102
assert!(!absent_signers.contains(&destination.to_string()));
30833103
assert!(!absent_signers.contains(&token.to_string()));
3104+
3105+
// now send the transaction
3106+
let program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>> = Arc::new(
3107+
ProgramRpcClient::new(config.rpc_client.clone(), ProgramRpcClientSendTransaction),
3108+
);
3109+
config.program_client = program_client;
3110+
let mut args = vec![
3111+
"spl-token".to_string(),
3112+
CommandName::Transfer.as_ref().to_string(),
3113+
token.to_string(),
3114+
"10".to_string(),
3115+
destination.to_string(),
3116+
"--blockhash".to_string(),
3117+
blockhash.to_string(),
3118+
"--nonce".to_string(),
3119+
nonce.to_string(),
3120+
"--nonce-authority".to_string(),
3121+
fee_payer_keypair_file.path().to_str().unwrap().to_string(),
3122+
"--mint-decimals".to_string(),
3123+
format!("{}", TEST_DECIMALS),
3124+
"--multisig-signer".to_string(),
3125+
multisig_members[1].to_string(),
3126+
"--multisig-signer".to_string(),
3127+
multisig_paths[2].path().to_str().unwrap().to_string(),
3128+
"--from".to_string(),
3129+
source.to_string(),
3130+
"--owner".to_string(),
3131+
multisig_pubkey.to_string(),
3132+
"--fee-payer".to_string(),
3133+
fee_payer_keypair_file.path().to_str().unwrap().to_string(),
3134+
"--program-id".to_string(),
3135+
program_id.to_string(),
3136+
"--signer".to_string(),
3137+
signer.to_string(),
3138+
];
3139+
if let Some(compute_unit_price) = compute_unit_price {
3140+
args.push("--with-compute-unit-price".to_string());
3141+
args.push(compute_unit_price.to_string());
3142+
args.push("--with-compute-unit-limit".to_string());
3143+
args.push(10_000.to_string());
3144+
}
3145+
exec_test_cmd(&config, &args).await.unwrap();
3146+
3147+
let account = config.rpc_client.get_account(&source).await.unwrap();
3148+
let token_account = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
3149+
let amount = spl_token::ui_amount_to_amount(90.0, TEST_DECIMALS);
3150+
assert_eq!(token_account.base.amount, amount);
3151+
let account = config.rpc_client.get_account(&destination).await.unwrap();
3152+
let token_account = StateWithExtensionsOwned::<Account>::unpack(account.data).unwrap();
3153+
let amount = spl_token::ui_amount_to_amount(10.0, TEST_DECIMALS);
3154+
assert_eq!(token_account.base.amount, amount);
30843155
}
30853156
}
30863157

3158+
async fn offline_multisig_transfer_with_nonce(test_validator: &TestValidator, payer: &Keypair) {
3159+
do_offline_multisig_transfer(test_validator, payer, None).await;
3160+
do_offline_multisig_transfer(test_validator, payer, Some(10)).await;
3161+
}
3162+
30873163
async fn withdraw_excess_lamports_from_multisig(test_validator: &TestValidator, payer: &Keypair) {
30883164
let m = 3;
30893165
let n = 5u8;
@@ -4024,7 +4100,7 @@ async fn compute_budget(test_validator: &TestValidator, payer: &Keypair) {
40244100
for program_id in VALID_TOKEN_PROGRAM_IDS.iter() {
40254101
let mut config = test_config_with_default_signer(test_validator, payer, program_id);
40264102
config.compute_unit_price = Some(42);
4027-
config.compute_unit_limit = Some(30_000);
4103+
config.compute_unit_limit = ComputeUnitLimit::Static(30_000);
40284104
run_transfer_test(&config, payer).await;
40294105
}
40304106
}

0 commit comments

Comments
 (0)