#![cfg(feature = "test-sbf")]
pub mod setup;
use std::borrow::BorrowMut;
use mpl_core::{
accounts::BaseAssetV1,
fetch_external_plugin_adapter_data_info,
instructions::{
AddExternalPluginAdapterV1Builder, UpdatePluginV1Builder,
WriteExternalPluginAdapterDataV1Builder,
},
types::{
AppDataInitInfo, Attribute, Attributes, ExternalPluginAdapterInitInfo,
ExternalPluginAdapterKey, ExternalPluginAdapterSchema, FreezeDelegate, Plugin,
PluginAuthority, PluginAuthorityPair,
},
Asset,
};
pub use setup::*;
use solana_program::account_info::AccountInfo;
use solana_program_test::tokio;
use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction};
#[tokio::test]
async fn test_write_external_plugin_adapter_data_shrink_preserves_second_plugin() {
let mut context = program_test().start_with_context().await;
let owner = Keypair::new();
airdrop(&mut context, &owner.pubkey(), 10_000_000_000)
.await
.unwrap();
let asset = Keypair::new();
create_asset(
&mut context,
CreateAssetHelperArgs {
owner: Some(owner.pubkey()),
payer: None,
asset: &asset,
data_state: None,
name: None,
uri: None,
authority: None,
update_authority: None,
collection: None,
plugins: vec![],
external_plugin_adapters: vec![ExternalPluginAdapterInitInfo::AppData(
AppDataInitInfo {
init_plugin_authority: Some(PluginAuthority::UpdateAuthority),
data_authority: PluginAuthority::UpdateAuthority,
schema: Some(ExternalPluginAdapterSchema::Binary),
},
)],
},
)
.await
.unwrap();
let ix = AddExternalPluginAdapterV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.init_info(ExternalPluginAdapterInitInfo::AppData(AppDataInitInfo {
init_plugin_authority: Some(PluginAuthority::UpdateAuthority),
data_authority: PluginAuthority::Owner,
schema: Some(ExternalPluginAdapterSchema::Binary),
}))
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let large_data: Vec<u8> = (0..500).map(|i| (i % 256) as u8).collect();
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.key(ExternalPluginAdapterKey::AppData(
PluginAuthority::UpdateAuthority,
))
.data(large_data.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let second_plugin_data: Vec<u8> = vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.authority(Some(owner.pubkey()))
.key(ExternalPluginAdapterKey::AppData(PluginAuthority::Owner))
.data(second_plugin_data.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer, &owner],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let account_before = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_before = account_before.data.len();
println!("Account size before shrink: {}", size_before);
let asset_before = Asset::from_bytes(&account_before.data).unwrap();
assert_eq!(asset_before.external_plugin_adapter_list.app_data.len(), 2);
{
let mut account_copy = account_before.clone();
let binding = asset.pubkey();
let account_info = AccountInfo::new(
&binding,
false,
false,
&mut account_copy.lamports,
account_copy.data.borrow_mut(),
&account_copy.owner,
false,
);
let (data_offset, data_len) = fetch_external_plugin_adapter_data_info::<BaseAssetV1>(
&account_info,
None,
&ExternalPluginAdapterKey::AppData(PluginAuthority::Owner),
)
.unwrap();
let data_slice = &account_copy.data[data_offset..data_offset + data_len];
assert_eq!(
data_slice, &second_plugin_data,
"Second plugin data should be intact before shrink"
);
}
let small_data: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.key(ExternalPluginAdapterKey::AppData(
PluginAuthority::UpdateAuthority,
))
.data(small_data.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let account_after = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_after = account_after.data.len();
assert!(
size_after < size_before,
"Expected account to shrink from {} to {}, but it did not",
size_before,
size_after
);
let asset_after = Asset::from_bytes(&account_after.data)
.expect("Asset deserialization should succeed after shrink — registry must remain intact");
assert_eq!(
asset_after.external_plugin_adapter_list.app_data.len(),
2,
"Both AppData plugins should survive the shrink"
);
{
let mut account_copy = account_after.clone();
let binding = asset.pubkey();
let account_info = AccountInfo::new(
&binding,
false,
false,
&mut account_copy.lamports,
account_copy.data.borrow_mut(),
&account_copy.owner,
false,
);
let (data_offset, data_len) = fetch_external_plugin_adapter_data_info::<BaseAssetV1>(
&account_info,
None,
&ExternalPluginAdapterKey::AppData(PluginAuthority::Owner),
)
.expect("Should be able to fetch second plugin data after shrink");
assert!(
data_offset + data_len <= account_after.data.len(),
"Second plugin data out of bounds: offset={} len={} account_size={}",
data_offset,
data_len,
account_after.data.len()
);
let actual_data = &account_after.data[data_offset..data_offset + data_len];
assert_eq!(
actual_data, &second_plugin_data,
"Second plugin data must be unchanged after shrinking the first plugin"
);
}
{
let mut account_copy = account_after.clone();
let binding = asset.pubkey();
let account_info = AccountInfo::new(
&binding,
false,
false,
&mut account_copy.lamports,
account_copy.data.borrow_mut(),
&account_copy.owner,
false,
);
let (data_offset, data_len) = fetch_external_plugin_adapter_data_info::<BaseAssetV1>(
&account_info,
None,
&ExternalPluginAdapterKey::AppData(PluginAuthority::UpdateAuthority),
)
.expect("Should be able to fetch first plugin data after shrink");
assert!(
data_offset + data_len <= account_after.data.len(),
"First plugin data out of bounds: offset={} len={} account_size={}",
data_offset,
data_len,
account_after.data.len()
);
let actual_data = &account_after.data[data_offset..data_offset + data_len];
assert_eq!(
actual_data, &small_data,
"First plugin data must equal the shrunk payload"
);
}
}
#[tokio::test]
async fn test_update_plugin_shrink_attributes_preserves_trailing_plugins() {
let mut context = program_test().start_with_context().await;
let asset = Keypair::new();
let large_attributes: Vec<Attribute> = (0..30)
.map(|i| Attribute {
key: format!("key_{:03}", i),
value: format!(
"value_{:03}_padding_to_make_this_larger_{}",
i,
"x".repeat(20)
),
})
.collect();
create_asset(
&mut context,
CreateAssetHelperArgs {
owner: None,
payer: None,
asset: &asset,
data_state: None,
name: None,
uri: None,
authority: None,
update_authority: None,
collection: None,
plugins: vec![
PluginAuthorityPair {
plugin: Plugin::Attributes(Attributes {
attribute_list: large_attributes.clone(),
}),
authority: None,
},
PluginAuthorityPair {
plugin: Plugin::FreezeDelegate(FreezeDelegate { frozen: false }),
authority: None,
},
],
external_plugin_adapters: vec![],
},
)
.await
.unwrap();
let account_before = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_before = account_before.data.len();
println!("Account size before shrink: {}", size_before);
let asset_before = Asset::from_bytes(&account_before.data).unwrap();
assert!(asset_before.plugin_list.freeze_delegate.is_some());
let attrs = asset_before.plugin_list.attributes.as_ref().unwrap();
assert_eq!(attrs.attributes.attribute_list.len(), 30);
let small_attributes = vec![Attribute {
key: "x".to_string(),
value: "y".to_string(),
}];
let ix = UpdatePluginV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.plugin(Plugin::Attributes(Attributes {
attribute_list: small_attributes.clone(),
}))
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context
.banks_client
.process_transaction(tx)
.await
.expect("Attributes shrink transaction should succeed");
let account_after = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_after = account_after.data.len();
assert!(
size_after < size_before,
"Expected account to shrink from {} to {}, but it did not",
size_before,
size_after
);
let asset_after = Asset::from_bytes(&account_after.data)
.expect("Asset deserialization should succeed after Attributes shrink");
let attrs_after = asset_after
.plugin_list
.attributes
.as_ref()
.expect("Attributes plugin must still be present after shrink");
assert_eq!(
attrs_after.attributes.attribute_list.len(),
1,
"Attributes should have exactly 1 entry after update"
);
assert_eq!(attrs_after.attributes.attribute_list[0].key, "x");
assert_eq!(attrs_after.attributes.attribute_list[0].value, "y");
let fd = asset_after
.plugin_list
.freeze_delegate
.as_ref()
.expect("FreezeDelegate must still be present after Attributes shrink");
assert_eq!(
fd.freeze_delegate,
FreezeDelegate { frozen: false },
"FreezeDelegate should be unchanged"
);
}
#[tokio::test]
async fn test_write_external_plugin_adapter_data_single_plugin_shrink() {
let mut context = program_test().start_with_context().await;
let asset = Keypair::new();
create_asset(
&mut context,
CreateAssetHelperArgs {
owner: None,
payer: None,
asset: &asset,
data_state: None,
name: None,
uri: None,
authority: None,
update_authority: None,
collection: None,
plugins: vec![],
external_plugin_adapters: vec![ExternalPluginAdapterInitInfo::AppData(
AppDataInitInfo {
init_plugin_authority: Some(PluginAuthority::UpdateAuthority),
data_authority: PluginAuthority::UpdateAuthority,
schema: Some(ExternalPluginAdapterSchema::Binary),
},
)],
},
)
.await
.unwrap();
let large_data: Vec<u8> = vec![0xAB; 800];
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.key(ExternalPluginAdapterKey::AppData(
PluginAuthority::UpdateAuthority,
))
.data(large_data.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let account_before = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_before = account_before.data.len();
println!("Account size before shrink: {}", size_before);
let asset_before = Asset::from_bytes(&account_before.data).unwrap();
assert_eq!(asset_before.external_plugin_adapter_list.app_data.len(), 1);
let small_data: Vec<u8> = vec![0x01];
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.key(ExternalPluginAdapterKey::AppData(
PluginAuthority::UpdateAuthority,
))
.data(small_data.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context
.banks_client
.process_transaction(tx)
.await
.expect("Shrink transaction should succeed — program must handle shrinking gracefully");
let account_after = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_after = account_after.data.len();
assert!(
size_after < size_before,
"Expected account to shrink from {} to {}, but it did not",
size_before,
size_after
);
let asset_after = Asset::from_bytes(&account_after.data)
.expect("Asset deserialization should succeed after single-plugin shrink");
assert_eq!(
asset_after.external_plugin_adapter_list.app_data.len(),
1,
"AppData plugin must still be present after shrink"
);
{
let mut account_copy = account_after.clone();
let binding = asset.pubkey();
let account_info = AccountInfo::new(
&binding,
false,
false,
&mut account_copy.lamports,
account_copy.data.borrow_mut(),
&account_copy.owner,
false,
);
let (data_offset, data_len) = fetch_external_plugin_adapter_data_info::<BaseAssetV1>(
&account_info,
None,
&ExternalPluginAdapterKey::AppData(PluginAuthority::UpdateAuthority),
)
.expect("Should be able to fetch AppData after single-plugin shrink");
assert!(
data_offset + data_len <= account_after.data.len(),
"AppData region out of bounds: offset={} len={} account_size={}",
data_offset,
data_len,
account_after.data.len()
);
let actual = &account_after.data[data_offset..data_offset + data_len];
assert_eq!(
actual, &small_data,
"AppData content must match the shrunk payload"
);
}
}
#[tokio::test]
async fn test_update_plugin_shrink_attributes_preserves_external_plugin() {
let mut context = program_test().start_with_context().await;
let asset = Keypair::new();
let large_attributes: Vec<Attribute> = (0..25)
.map(|i| Attribute {
key: format!("attr_{:03}", i),
value: format!("val_{:03}_{}", i, "abcdefghijklmnopqrstuvwxyz".repeat(2)),
})
.collect();
create_asset(
&mut context,
CreateAssetHelperArgs {
owner: None,
payer: None,
asset: &asset,
data_state: None,
name: None,
uri: None,
authority: None,
update_authority: None,
collection: None,
plugins: vec![PluginAuthorityPair {
plugin: Plugin::Attributes(Attributes {
attribute_list: large_attributes.clone(),
}),
authority: None,
}],
external_plugin_adapters: vec![ExternalPluginAdapterInitInfo::AppData(
AppDataInitInfo {
init_plugin_authority: Some(PluginAuthority::UpdateAuthority),
data_authority: PluginAuthority::UpdateAuthority,
schema: Some(ExternalPluginAdapterSchema::Binary),
},
)],
},
)
.await
.unwrap();
let app_data_content: Vec<u8> = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE];
let ix = WriteExternalPluginAdapterDataV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.key(ExternalPluginAdapterKey::AppData(
PluginAuthority::UpdateAuthority,
))
.data(app_data_content.clone())
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context.banks_client.process_transaction(tx).await.unwrap();
let account_before = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_before = account_before.data.len();
let asset_before = Asset::from_bytes(&account_before.data).unwrap();
assert!(asset_before.plugin_list.attributes.is_some());
assert_eq!(asset_before.external_plugin_adapter_list.app_data.len(), 1);
println!("Account size before shrink: {}", size_before);
let small_attributes = vec![Attribute {
key: "a".to_string(),
value: "b".to_string(),
}];
let ix = UpdatePluginV1Builder::new()
.asset(asset.pubkey())
.payer(context.payer.pubkey())
.plugin(Plugin::Attributes(Attributes {
attribute_list: small_attributes,
}))
.instruction();
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&context.payer.pubkey()),
&[&context.payer],
context.last_blockhash,
);
context
.banks_client
.process_transaction(tx)
.await
.expect("Attributes shrink transaction should succeed");
let account_after = context
.banks_client
.get_account(asset.pubkey())
.await
.unwrap()
.unwrap();
let size_after = account_after.data.len();
assert!(
size_after < size_before,
"Expected account to shrink from {} to {}, but it did not",
size_before,
size_after
);
let asset_after = Asset::from_bytes(&account_after.data)
.expect("Asset deserialization should succeed after Attributes shrink");
let attrs_after = asset_after
.plugin_list
.attributes
.as_ref()
.expect("Attributes plugin must still be present after shrink");
assert_eq!(
attrs_after.attributes.attribute_list.len(),
1,
"Attributes should have exactly 1 entry after update"
);
assert_eq!(attrs_after.attributes.attribute_list[0].key, "a");
assert_eq!(attrs_after.attributes.attribute_list[0].value, "b");
assert_eq!(
asset_after.external_plugin_adapter_list.app_data.len(),
1,
"AppData plugin must still be present after Attributes shrink"
);
{
let mut account_copy = account_after.clone();
let binding = asset.pubkey();
let account_info = AccountInfo::new(
&binding,
false,
false,
&mut account_copy.lamports,
account_copy.data.borrow_mut(),
&account_copy.owner,
false,
);
let (data_offset, data_len) = fetch_external_plugin_adapter_data_info::<BaseAssetV1>(
&account_info,
None,
&ExternalPluginAdapterKey::AppData(PluginAuthority::UpdateAuthority),
)
.expect("Should be able to fetch AppData after Attributes shrink");
assert!(
data_offset + data_len <= account_after.data.len(),
"AppData region out of bounds: offset={} len={} account_size={}",
data_offset,
data_len,
account_after.data.len()
);
let actual = &account_after.data[data_offset..data_offset + data_len];
assert_eq!(
actual, &app_data_content,
"AppData content must be unchanged after Attributes shrink"
);
}
}