662 lines
28 KiB
Rust
662 lines
28 KiB
Rust
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/fixture.rs
|
|
// version: 5
|
|
|
|
//! Native Token-2022 mint preparation for the embedded Token Metadata Devnet campaign.
|
|
|
|
/// Stable metadata name used by the `0.4.8-pre.012` campaign.
|
|
pub const TOKEN_2022_METADATA_CAMPAIGN_NAME: &str = "Khadhroony Token-2022 Metadata";
|
|
/// Stable metadata symbol used by the `0.4.8-pre.012` campaign.
|
|
pub const TOKEN_2022_METADATA_CAMPAIGN_SYMBOL: &str = "KBTM";
|
|
/// Opaque URI stored on-chain by the `0.4.8-pre.012` campaign.
|
|
pub const TOKEN_2022_METADATA_CAMPAIGN_URI: &str =
|
|
"https://example.invalid/khadhroony/token-2022-metadata.json";
|
|
/// Additional metadata key created and removed by the campaign.
|
|
pub const TOKEN_2022_METADATA_CAMPAIGN_KEY: &str = "campaign";
|
|
/// Additional metadata value created and removed by the campaign.
|
|
pub const TOKEN_2022_METADATA_CAMPAIGN_VALUE: &str = "0.4.8-pre.012";
|
|
|
|
const TOKEN_2022_TLV_HEADER_BYTES: usize = 4;
|
|
|
|
/// Options used to prepare one fresh Token-2022 mint for the metadata campaign.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct Token2022MetadataFixturePreparationOptions {
|
|
/// Endpoint role used for state, rent, balance, blockhash and fee calls.
|
|
pub query_role: std::string::String,
|
|
/// Endpoint role used for simulation, submission and confirmation polling.
|
|
pub transaction_role: std::string::String,
|
|
/// Directory receiving persistent fixture keypairs.
|
|
pub wallet_dir: std::path::PathBuf,
|
|
/// Explicit operator authorization for the lamport-spending fixture transaction.
|
|
pub operator_confirmed: bool,
|
|
}
|
|
|
|
impl crate::Token2022MetadataFixturePreparationOptions {
|
|
/// Creates conservative defaults matching Devnet execution profiles.
|
|
pub fn new(wallet_dir: std::path::PathBuf) -> Self {
|
|
return Self {
|
|
query_role: "http_queries".to_string(),
|
|
transaction_role: "http_transactions".to_string(),
|
|
wallet_dir,
|
|
operator_confirmed: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Fresh Token-2022 mint and authority identities used by the metadata campaign.
|
|
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
|
|
pub struct Token2022MetadataFixturePreparationSummary {
|
|
/// Persistent fixture mint keypair path.
|
|
pub mint_keypair_path: std::path::PathBuf,
|
|
/// Persistent final metadata-authority keypair path.
|
|
pub final_authority_keypair_path: std::path::PathBuf,
|
|
/// Fresh Token-2022 mint address. Embedded metadata uses this same account.
|
|
pub mint: ks_lib::MdPubkey,
|
|
/// Profile wallet used as mint and initial metadata update authority.
|
|
pub initial_authority: ks_lib::MdPubkey,
|
|
/// Fresh public key installed by the final `UpdateAuthority` campaign step.
|
|
pub final_authority: ks_lib::MdPubkey,
|
|
/// Initial mint account bytes containing only the fixed Metadata Pointer extension.
|
|
pub mint_space: usize,
|
|
/// Account-size budget used to pre-fund rent for the largest campaign metadata state.
|
|
pub rent_budget_space: usize,
|
|
/// Rent reserve deposited when the mint is created.
|
|
pub rent_reserve_lamports: u64,
|
|
/// Confirmed signature that created the Token-2022 mint fixture.
|
|
pub preparation_signature: std::string::String,
|
|
/// Authoritative snapshot after mint creation and before Token Metadata initialization.
|
|
pub initial_snapshot: ks_pipeline::Token2022StatefulReadResult,
|
|
}
|
|
|
|
/// Creates a fresh Token-2022 mint with Metadata Pointer initialized to the mint itself.
|
|
///
|
|
/// The mint account is created with the fixed Metadata Pointer size but is pre-funded
|
|
/// for the largest state reached by this campaign so Token Metadata reallocations
|
|
/// remain rent-exempt without introducing an additional payer instruction.
|
|
pub async fn prepare_token_2022_metadata_fixture(
|
|
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
|
profile: &ks_config::ProfileConfig,
|
|
workspace_root: &std::path::Path,
|
|
options: &crate::Token2022MetadataFixturePreparationOptions,
|
|
) -> ks_core::Result<crate::Token2022MetadataFixturePreparationSummary> {
|
|
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"Token-2022 metadata fixture endpoint roles must not be empty",
|
|
));
|
|
}
|
|
if profile.wallet.cluster != "devnet"
|
|
|| !profile.wallet.temporary_wallet_enabled
|
|
|| !profile.wallet.temporary_wallet_persist
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"Token-2022 metadata fixture requires a persistent Devnet wallet profile",
|
|
));
|
|
}
|
|
if !profile.execution.devnet_send_enabled || !options.operator_confirmed {
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"Token-2022 metadata fixture requires enabled Devnet submission and explicit operator confirmation",
|
|
));
|
|
}
|
|
let operator = match crate::load_profile_wallet(profile, workspace_root).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let fixture_store = match ks_wallet::TemporaryWalletStore::new(
|
|
options.wallet_dir.join("token_2022_metadata_validation"),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let fixture_epoch = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
|
std::result::Result::Ok(value) => value.as_millis(),
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"unable to create a unique Token-2022 metadata fixture identifier: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let mint_alias = match ks_wallet::WalletAlias::parse(
|
|
format!("token-2022-metadata-mint-{fixture_epoch}").as_str(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let final_authority_alias = match ks_wallet::WalletAlias::parse(
|
|
format!("token-2022-metadata-authority-{fixture_epoch}").as_str(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mint_wallet = match fixture_store.load_or_create(mint_alias.clone()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let final_authority_wallet =
|
|
match fixture_store.load_or_create(final_authority_alias.clone()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mint = ks_lib::MdPubkey(mint_wallet.public_key());
|
|
let initial_authority = ks_lib::MdPubkey(operator.public_key());
|
|
let final_authority = ks_lib::MdPubkey(final_authority_wallet.public_key());
|
|
let mint_space = match token_2022_metadata_pointer_mint_space() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let rent_budget_space = metadata_campaign_rent_budget_space(mint_space);
|
|
let preparation_signature = match create_native_token_2022_metadata_mint(
|
|
http_pool,
|
|
profile,
|
|
options,
|
|
&operator,
|
|
&mint_wallet,
|
|
mint_space,
|
|
rent_budget_space,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let initial_snapshot = match ks_pipeline::read_token_2022_stateful_snapshot(
|
|
http_pool,
|
|
&ks_pipeline::Token2022StatefulReadRequest {
|
|
query_role: options.query_role.clone(),
|
|
account: mint.clone(),
|
|
kind: ks_lib::DcToken2022StateKind::Mint,
|
|
min_context_slot: std::option::Option::None,
|
|
max_data_bytes: ks_pipeline::MAX_TOKEN_2022_STATEFUL_ACCOUNT_BYTES,
|
|
context: ks_pipeline::Token2022StatefulContext::default(),
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if !initial_snapshot
|
|
.snapshot
|
|
.extension_names
|
|
.iter()
|
|
.any(|value| return value == "metadata_pointer")
|
|
|| initial_snapshot
|
|
.snapshot
|
|
.extension_names
|
|
.iter()
|
|
.any(|value| return value == "token_metadata")
|
|
|| !metadata_pointer_snapshot_matches(&initial_snapshot.snapshot, &mint, &initial_authority)
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_state_invalid",
|
|
"fresh metadata fixture must contain the expected self-referential Metadata Pointer and no Token Metadata extension",
|
|
));
|
|
}
|
|
let rent = match http_pool
|
|
.get_minimum_balance_for_rent_exemption_for_role(
|
|
options.query_role.as_str(),
|
|
match u64::try_from(rent_budget_space) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"Token-2022 metadata rent budget does not fit u64: {error}"
|
|
)));
|
|
},
|
|
},
|
|
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(crate::Token2022MetadataFixturePreparationSummary {
|
|
mint_keypair_path: fixture_store.wallet_path(&mint_alias),
|
|
final_authority_keypair_path: fixture_store.wallet_path(&final_authority_alias),
|
|
mint,
|
|
initial_authority,
|
|
final_authority,
|
|
mint_space,
|
|
rent_budget_space,
|
|
rent_reserve_lamports: rent.minimum_balance_lamports,
|
|
preparation_signature,
|
|
initial_snapshot,
|
|
});
|
|
}
|
|
|
|
fn metadata_pointer_snapshot_matches(
|
|
snapshot: &ks_pipeline::Token2022StatefulSnapshotBundle,
|
|
mint: &ks_lib::MdPubkey,
|
|
authority: &ks_lib::MdPubkey,
|
|
) -> bool {
|
|
return snapshot.outputs.iter().any(|output| {
|
|
return output.family == ks_lib::MdMaterializedEventFamily::Admin
|
|
&& output.payload_json.get("extensionName").and_then(serde_json::Value::as_str)
|
|
== std::option::Option::Some("metadata_pointer")
|
|
&& output
|
|
.payload_json
|
|
.get("valueFields")
|
|
.and_then(|value| return value.get("address"))
|
|
.and_then(serde_json::Value::as_str)
|
|
== std::option::Option::Some(mint.0.as_str())
|
|
&& output
|
|
.payload_json
|
|
.get("valueFields")
|
|
.and_then(|value| return value.get("authority"))
|
|
.and_then(serde_json::Value::as_str)
|
|
== std::option::Option::Some(authority.0.as_str());
|
|
});
|
|
}
|
|
|
|
fn token_2022_metadata_pointer_mint_space() -> ks_core::Result<usize> {
|
|
return match spl_token_2022_interface::extension::ExtensionType::try_calculate_account_len::<
|
|
spl_token_2022_interface::pod::PodMint,
|
|
>(&[spl_token_2022_interface::extension::ExtensionType::MetadataPointer])
|
|
{
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_space_failed",
|
|
format!("unable to calculate Metadata Pointer mint space: {error}"),
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn metadata_campaign_rent_budget_space(mint_space: usize) -> usize {
|
|
return mint_space
|
|
.saturating_add(TOKEN_2022_TLV_HEADER_BYTES)
|
|
.saturating_add(metadata_campaign_max_value_bytes());
|
|
}
|
|
|
|
fn metadata_campaign_max_value_bytes() -> usize {
|
|
return 64_usize
|
|
.saturating_add(4)
|
|
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_NAME.len())
|
|
.saturating_add(4)
|
|
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_SYMBOL.len())
|
|
.saturating_add(4)
|
|
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_URI.len())
|
|
.saturating_add(4)
|
|
.saturating_add(4)
|
|
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_KEY.len())
|
|
.saturating_add(4)
|
|
.saturating_add(TOKEN_2022_METADATA_CAMPAIGN_VALUE.len());
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
async fn create_native_token_2022_metadata_mint(
|
|
http_pool: &ks_onchain_transport::HttpEndpointPool,
|
|
profile: &ks_config::ProfileConfig,
|
|
options: &crate::Token2022MetadataFixturePreparationOptions,
|
|
operator: &ks_wallet::TemporaryWallet,
|
|
mint_wallet: &ks_wallet::TemporaryWallet,
|
|
mint_space: usize,
|
|
rent_budget_space: usize,
|
|
) -> ks_core::Result<std::string::String> {
|
|
let rent_space = match u64::try_from(rent_budget_space) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"Token-2022 metadata rent budget does not fit u64: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let account_space = match u64::try_from(mint_space) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"Token-2022 Metadata Pointer mint space does not fit u64: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let rent = match http_pool
|
|
.get_minimum_balance_for_rent_exemption_for_role(
|
|
options.query_role.as_str(),
|
|
rent_space,
|
|
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let payer = ks_lib::MdPubkey(operator.public_key());
|
|
let mint = ks_lib::MdPubkey(mint_wallet.public_key());
|
|
let policy =
|
|
fixture_policy(profile, payer.clone(), mint.clone(), rent.minimum_balance_lamports);
|
|
let system_intent = ks_lib::ExSolanaCoreExecutionIntent {
|
|
intent_id: format!("token-2022-metadata-fixture-create-account-{}", mint.0),
|
|
fee_payer: payer.clone(),
|
|
policy: policy.clone(),
|
|
operation: ks_lib::ExSolanaCoreOperation::SystemCreateAccount {
|
|
from: payer.clone(),
|
|
new_account: mint.clone(),
|
|
lamports: rent.minimum_balance_lamports,
|
|
space: account_space,
|
|
owner: ks_lib::MdPubkey(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string()),
|
|
},
|
|
};
|
|
let system_plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&ks_lib::ExSolanaCoreExecutor,
|
|
&system_intent,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let token_program = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(
|
|
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"invalid Token-2022 Program ID: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let mint_pubkey = match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(mint.0.as_str())
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"invalid Token-2022 metadata fixture mint: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let authority_pubkey =
|
|
match <solana_pubkey::Pubkey as std::str::FromStr>::from_str(payer.0.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::config(format!(
|
|
"invalid Token-2022 metadata fixture authority: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let pointer_instruction =
|
|
match spl_token_2022_interface::extension::metadata_pointer::instruction::initialize(
|
|
&token_program,
|
|
&mint_pubkey,
|
|
std::option::Option::Some(authority_pubkey),
|
|
std::option::Option::Some(mint_pubkey),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_pointer_build_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let initialize_mint = match spl_token_2022_interface::instruction::initialize_mint2(
|
|
&token_program,
|
|
&mint_pubkey,
|
|
&authority_pubkey,
|
|
std::option::Option::Some(&authority_pubkey),
|
|
0,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_initialize_mint_build_failed",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let plan = merge_fixture_instructions(
|
|
system_plan,
|
|
std::vec![
|
|
planned_instruction("spl.token_2022.initialize_metadata_pointer", &pointer_instruction,),
|
|
planned_instruction("spl.token_2022.initialize_mint2", &initialize_mint),
|
|
],
|
|
policy,
|
|
rent.minimum_balance_lamports,
|
|
);
|
|
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_plan_denied",
|
|
crate::violation_message(plan_evaluation.violations.as_slice()),
|
|
));
|
|
}
|
|
let latest_blockhash = match http_pool
|
|
.get_latest_blockhash_for_role(
|
|
options.query_role.as_str(),
|
|
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
|
|
&plan,
|
|
latest_blockhash.blockhash.as_str(),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let fee = match http_pool
|
|
.get_fee_for_message_for_role(
|
|
options.query_role.as_str(),
|
|
unsigned.message_base64().as_str(),
|
|
&ks_onchain_transport::GetFeeForMessageConfig::new(
|
|
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
|
|
std::option::Option::Some(latest_blockhash.context.slot),
|
|
),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let unsigned_base64 = match unsigned.transaction_base64() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
|
|
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
|
|
false,
|
|
false,
|
|
std::option::Option::Some(latest_blockhash.context.slot),
|
|
true,
|
|
std::option::Option::None,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let simulation_rpc = match http_pool
|
|
.simulate_transaction_for_role(
|
|
options.transaction_role.as_str(),
|
|
unsigned_base64.as_str(),
|
|
&simulation_config,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let simulation = simulation_rpc.to_execution_result(
|
|
ks_lib::ExApiExecutionCluster::Devnet,
|
|
ks_lib::ExApiExecutionBlockhashKind::Latest,
|
|
std::option::Option::Some(
|
|
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
|
|
),
|
|
std::option::Option::None,
|
|
std::option::Option::None,
|
|
std::option::Option::Some(&fee),
|
|
);
|
|
if !simulation.success {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_simulation_failed",
|
|
crate::simulation_failure_message(&simulation),
|
|
));
|
|
}
|
|
let send_evaluation = match ks_lib::ExSafetyChecker.evaluate_send(&plan, &simulation) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_send_denied",
|
|
crate::violation_message(send_evaluation.violations.as_slice()),
|
|
));
|
|
}
|
|
let evidence = unsigned.bind_simulation(simulation);
|
|
let signed = match unsigned
|
|
.sign_after_simulation(&evidence, &[operator.as_signer(), mint_wallet.as_signer()])
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = signed.verify_signatures() {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let signature = signed.primary_signature().clone();
|
|
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
|
|
&profile.execution,
|
|
std::option::Option::Some(latest_blockhash.context.slot),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = http_pool
|
|
.send_transaction_for_role(
|
|
options.transaction_role.as_str(),
|
|
signed.transaction_base64().as_str(),
|
|
&signature,
|
|
&send_config,
|
|
)
|
|
.await
|
|
{
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let confirmation_config =
|
|
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
|
|
&profile.execution,
|
|
std::option::Option::Some(latest_blockhash.last_valid_block_height),
|
|
std::option::Option::Some(latest_blockhash.context.slot),
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let confirmation = match http_pool
|
|
.confirm_transaction_for_roles(
|
|
options.transaction_role.as_str(),
|
|
options.query_role.as_str(),
|
|
ks_lib::ExApiExecutionCluster::Devnet,
|
|
&signature,
|
|
&confirmation_config,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if !matches!(
|
|
confirmation.status,
|
|
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
|
|
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
|
|
) {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"token_2022_metadata_fixture_confirmation_failed",
|
|
format!("Token-2022 metadata mint preparation stopped at {:?}", confirmation.status),
|
|
));
|
|
}
|
|
return std::result::Result::Ok(signature.0);
|
|
}
|
|
|
|
fn planned_instruction(
|
|
operation_code: &str,
|
|
instruction: &solana_instruction::Instruction,
|
|
) -> ks_lib::ExApiPlannedInstruction {
|
|
return ks_lib::ExApiPlannedInstruction {
|
|
program_id: ks_lib::MdProgramId(instruction.program_id.to_string()),
|
|
operation_code: operation_code.to_string(),
|
|
accounts: instruction
|
|
.accounts
|
|
.iter()
|
|
.map(|account| {
|
|
return ks_lib::ExApiPlannedAccount {
|
|
pubkey: ks_lib::MdPubkey(account.pubkey.to_string()),
|
|
is_signer: account.is_signer,
|
|
is_writable: account.is_writable,
|
|
};
|
|
})
|
|
.collect(),
|
|
data: instruction.data.clone(),
|
|
};
|
|
}
|
|
|
|
fn fixture_policy(
|
|
profile: &ks_config::ProfileConfig,
|
|
payer: ks_lib::MdPubkey,
|
|
mint: ks_lib::MdPubkey,
|
|
_rent_lamports: u64,
|
|
) -> ks_lib::ExApiExecutionPolicy {
|
|
return ks_lib::ExApiExecutionPolicy {
|
|
cluster: ks_lib::ExApiExecutionClusterPolicy {
|
|
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
|
|
allow_mainnet: false,
|
|
mainnet_confirmation: false,
|
|
},
|
|
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
|
|
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
|
|
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
|
|
max_age_slots: std::option::Option::Some(
|
|
profile.execution.recent_blockhash_max_age_slots,
|
|
),
|
|
nonce_account: std::option::Option::None,
|
|
nonce_authority: std::option::Option::None,
|
|
},
|
|
cost_limit: ks_lib::ExApiExecutionCostLimit {
|
|
max_spend_lamports: std::option::Option::Some(
|
|
profile.execution.devnet_max_spend_lamports,
|
|
),
|
|
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
|
|
max_compute_unit_price_micro_lamports: std::option::Option::Some(
|
|
profile.execution.max_compute_unit_price_micro_lamports,
|
|
),
|
|
},
|
|
authorized_signers: std::vec![payer, mint],
|
|
dry_run: false,
|
|
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
|
|
canonical_insert_required: false,
|
|
core_extraction_required: false,
|
|
decode_replay_required: false,
|
|
materialization_required: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn merge_fixture_instructions(
|
|
mut system_plan: ks_lib::ExApiPreparedExecutionPlan,
|
|
token_instructions: std::vec::Vec<ks_lib::ExApiPlannedInstruction>,
|
|
policy: ks_lib::ExApiExecutionPolicy,
|
|
rent_lamports: u64,
|
|
) -> ks_lib::ExApiPreparedExecutionPlan {
|
|
system_plan.instructions.extend(token_instructions);
|
|
system_plan.executor_name =
|
|
"ks-pipeline-demo-scenarios.token_2022_metadata_fixture".to_string();
|
|
system_plan.operation_code = "spl.token_2022.metadata.prepare_mint".to_string();
|
|
system_plan.policy = policy;
|
|
system_plan.requested_spend_lamports = rent_lamports;
|
|
return system_plan;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn metadata_rent_budget_covers_pointer_and_largest_campaign_value() {
|
|
let mint_space = super::token_2022_metadata_pointer_mint_space()
|
|
.unwrap_or_else(|error| panic!("metadata pointer mint size failed: {error}"));
|
|
assert!(mint_space > 82);
|
|
let budget = super::metadata_campaign_rent_budget_space(mint_space);
|
|
assert_eq!(
|
|
budget,
|
|
mint_space
|
|
+ super::TOKEN_2022_TLV_HEADER_BYTES
|
|
+ super::metadata_campaign_max_value_bytes()
|
|
);
|
|
assert!(budget > mint_space);
|
|
}
|
|
}
|