use std::marker::PhantomData;
use light_compressed_account::{
compressed_account::PackedMerkleContext,
instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo},
};
use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait;
use solana_pubkey::Pubkey;
#[cfg(feature = "poseidon")]
use crate::light_hasher::Poseidon;
use crate::{
error::LightSdkError,
light_hasher::{DataHasher, Hasher, Sha256},
AnchorDeserialize, AnchorSerialize, LightDiscriminator,
};
const DEFAULT_DATA_HASH: [u8; 32] = [0u8; 32];
pub trait Size {
fn size(&self) -> Result<usize, solana_program_error::ProgramError>;
}
pub use sha::LightAccount;
pub mod sha {
use super::*;
pub type LightAccount<A> = super::LightAccountInner<Sha256, A, true>;
}
#[cfg(feature = "poseidon")]
pub mod poseidon {
use super::*;
pub type LightAccount<A> = super::LightAccountInner<Poseidon, A, false>;
}
#[doc(hidden)]
pub use __internal::LightAccountInner;
#[doc(hidden)]
pub mod __internal {
use light_compressed_account::instruction_data::{
data::OutputCompressedAccountWithPackedContext, with_readonly::InAccount,
};
use light_sdk_types::instruction::account_meta::CompressedAccountMetaBurn;
#[cfg(feature = "v2")]
use light_sdk_types::instruction::account_meta::CompressedAccountMetaReadOnly;
use solana_program_error::ProgramError;
use super::*;
#[doc(hidden)]
#[derive(Debug, PartialEq)]
pub struct LightAccountInner<
H: Hasher,
A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
const HASH_FLAT: bool,
> {
owner: Pubkey,
pub account: A,
account_info: CompressedAccountInfo,
should_remove_data: bool,
pub read_only_account_hash: Option<[u8; 32]>,
_hasher: PhantomData<H>,
}
impl<
H: Hasher,
A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
const HASH_FLAT: bool,
> core::ops::Deref for LightAccountInner<H, A, HASH_FLAT>
{
type Target = A;
fn deref(&self) -> &Self::Target {
&self.account
}
}
impl<
H: Hasher,
A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
const HASH_FLAT: bool,
> core::ops::DerefMut for LightAccountInner<H, A, HASH_FLAT>
{
fn deref_mut(&mut self) -> &mut Self::Target {
assert!(
self.read_only_account_hash.is_none(),
"Cannot mutate read-only account"
);
&mut self.account
}
}
impl<
H: Hasher,
A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
const HASH_FLAT: bool,
> LightAccountInner<H, A, HASH_FLAT>
{
pub fn new_init(
owner: &impl crate::PubkeyTrait,
address: Option<[u8; 32]>,
output_state_tree_index: u8,
) -> Self {
let output_account_info = OutAccountInfo {
output_merkle_tree_index: output_state_tree_index,
discriminator: A::LIGHT_DISCRIMINATOR,
..Default::default()
};
Self {
owner: owner.to_solana_pubkey(),
account: A::default(),
account_info: CompressedAccountInfo {
address,
input: None,
output: Some(output_account_info),
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
}
}
pub fn discriminator(&self) -> &[u8; 8] {
&A::LIGHT_DISCRIMINATOR
}
pub fn lamports(&self) -> u64 {
if let Some(output) = self.account_info.output.as_ref() {
output.lamports
} else if let Some(input) = self.account_info.input.as_ref() {
input.lamports
} else {
0
}
}
pub fn lamports_mut(&mut self) -> &mut u64 {
if let Some(output) = self.account_info.output.as_mut() {
&mut output.lamports
} else if let Some(input) = self.account_info.input.as_mut() {
&mut input.lamports
} else {
panic!("No lamports field available in account_info")
}
}
pub fn address(&self) -> &Option<[u8; 32]> {
&self.account_info.address
}
pub fn owner(&self) -> &Pubkey {
&self.owner
}
pub fn size(&self) -> Result<usize, solana_program_error::ProgramError>
where
A: Size,
{
self.account.size()
}
pub fn remove_data(&mut self) {
self.should_remove_data = true;
}
pub fn in_account_info(&self) -> &Option<InAccountInfo> {
&self.account_info.input
}
pub fn out_account_info(&mut self) -> &Option<OutAccountInfo> {
&self.account_info.output
}
}
impl<
H: Hasher,
A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default,
> LightAccountInner<H, A, false>
{
pub fn new_mut(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
input_account: A,
) -> Result<Self, LightSdkError> {
let input_account_info = {
let input_data_hash = input_account.hash::<H>()?;
let tree_info = input_account_meta.get_tree_info();
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
}
};
let output_account_info = {
let output_merkle_tree_index = input_account_meta
.get_output_state_tree_index()
.ok_or(LightSdkError::OutputStateTreeIndexIsNone)?;
OutAccountInfo {
lamports: input_account_meta.get_lamports().unwrap_or_default(),
output_merkle_tree_index,
discriminator: A::LIGHT_DISCRIMINATOR,
..Default::default()
}
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: Some(output_account_info),
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
})
}
pub fn new_empty(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
) -> Result<Self, LightSdkError> {
let input_account_info = {
let input_data_hash = DEFAULT_DATA_HASH;
let tree_info = input_account_meta.get_tree_info();
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: [0u8; 8],
}
};
let output_account_info = {
let output_merkle_tree_index = input_account_meta
.get_output_state_tree_index()
.ok_or(LightSdkError::OutputStateTreeIndexIsNone)?;
OutAccountInfo {
lamports: input_account_meta.get_lamports().unwrap_or_default(),
output_merkle_tree_index,
discriminator: A::LIGHT_DISCRIMINATOR,
..Default::default()
}
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: A::default(),
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: Some(output_account_info),
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
})
}
pub fn new_close(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
input_account: A,
) -> Result<Self, LightSdkError> {
let mut account = Self::new_mut(owner, input_account_meta, input_account)?;
account.should_remove_data = true;
Ok(account)
}
pub fn new_burn(
owner: &impl crate::PubkeyTrait,
input_account_meta: &CompressedAccountMetaBurn,
input_account: A,
) -> Result<Self, LightSdkError> {
let input_account_info = {
let input_data_hash = input_account.hash::<H>()?;
let tree_info = input_account_meta.get_tree_info();
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
}
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: None,
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
})
}
#[cfg(feature = "v2")]
pub fn new_read_only(
owner: &impl crate::PubkeyTrait,
input_account_meta: &CompressedAccountMetaReadOnly,
input_account: A,
packed_account_pubkeys: &[Pubkey],
) -> Result<Self, ProgramError> {
let input_data_hash = input_account
.hash::<H>()
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
let tree_info = input_account_meta.get_tree_info();
let input_account_info = InAccountInfo {
data_hash: input_data_hash,
lamports: 0, merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
};
let account_hash = {
use light_compressed_account::compressed_account::{
CompressedAccount, CompressedAccountData,
};
let compressed_account = CompressedAccount {
address: Some(input_account_meta.address),
owner: owner.to_array().into(),
data: Some(CompressedAccountData {
data: vec![], data_hash: input_data_hash, discriminator: A::LIGHT_DISCRIMINATOR,
}),
lamports: 0,
};
let merkle_tree_pubkey = packed_account_pubkeys
.get(tree_info.merkle_tree_pubkey_index as usize)
.ok_or(LightSdkError::InvalidMerkleTreeIndex)
.map_err(ProgramError::from)?
.to_bytes()
.into();
compressed_account
.hash(&merkle_tree_pubkey, &tree_info.leaf_index, true)
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: Some(input_account_meta.address),
input: Some(input_account_info),
output: None,
},
should_remove_data: false,
read_only_account_hash: Some(account_hash),
_hasher: PhantomData,
})
}
pub fn to_account_info(mut self) -> Result<CompressedAccountInfo, ProgramError> {
if self.read_only_account_hash.is_some() {
return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into());
}
if let Some(output) = self.account_info.output.as_mut() {
if self.should_remove_data {
if !output.data.is_empty() {
return Err(LightSdkError::ExpectedNoData.into());
}
output.data_hash = DEFAULT_DATA_HASH;
output.discriminator = [0u8; 8];
} else {
output.data = self
.account
.try_to_vec()
.map_err(|_| LightSdkError::Borsh)?;
output.data_hash = self
.account
.hash::<H>()
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
}
}
Ok(self.account_info)
}
#[cfg(feature = "v2")]
pub fn to_packed_read_only_account(
self,
) -> Result<
light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount,
ProgramError,
> {
let account_hash = self
.read_only_account_hash
.ok_or(LightSdkError::NotReadOnlyAccount)?;
let input_account = self
.account_info
.input
.ok_or(ProgramError::InvalidAccountData)?;
use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount;
Ok(PackedReadOnlyCompressedAccount {
root_index: input_account.root_index,
merkle_context: input_account.merkle_context,
account_hash,
})
}
pub fn to_in_account(&self) -> Option<InAccount> {
self.account_info
.input
.as_ref()
.map(|input| input.into_in_account(self.account_info.address))
}
pub fn to_output_compressed_account_with_packed_context(
&self,
owner: Option<solana_pubkey::Pubkey>,
) -> Result<Option<OutputCompressedAccountWithPackedContext>, ProgramError> {
let owner = if let Some(owner) = owner {
owner.to_bytes().into()
} else {
self.owner.to_bytes().into()
};
if let Some(mut output) = self.account_info.output.clone() {
if self.should_remove_data {
if !output.data.is_empty() {
return Err(LightSdkError::ExpectedNoData.into());
}
output.data_hash = DEFAULT_DATA_HASH;
output.discriminator = [0u8; 8];
} else {
output.data = self
.account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
output.data_hash = self
.account
.hash::<H>()
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
output.data = self
.account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
}
let result = OutputCompressedAccountWithPackedContext::from_with_owner(
&output,
owner,
self.account_info.address,
);
Ok(Some(result))
} else {
Ok(None)
}
}
}
impl<H: Hasher, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default>
LightAccountInner<H, A, true>
{
pub fn new_mut(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
input_account: A,
) -> Result<Self, ProgramError> {
Ok(Self::new_mut_inner(owner, input_account_meta, input_account)?.0)
}
pub fn new_empty(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
) -> Result<Self, ProgramError> {
let input_account_info = {
let input_data_hash = DEFAULT_DATA_HASH;
let tree_info = input_account_meta.get_tree_info();
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: [0u8; 8],
}
};
let output_account_info = {
let output_merkle_tree_index = input_account_meta
.get_output_state_tree_index()
.ok_or(LightSdkError::OutputStateTreeIndexIsNone)
.map_err(ProgramError::from)?;
OutAccountInfo {
lamports: input_account_meta.get_lamports().unwrap_or_default(),
output_merkle_tree_index,
discriminator: A::LIGHT_DISCRIMINATOR,
..Default::default()
}
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: A::default(),
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: Some(output_account_info),
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
})
}
pub fn new_close(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
input_account: A,
) -> Result<Self, ProgramError> {
let mut account = Self::new_mut(owner, input_account_meta, input_account)?;
account.should_remove_data = true;
Ok(account)
}
pub fn new_burn(
owner: &impl crate::PubkeyTrait,
input_account_meta: &CompressedAccountMetaBurn,
input_account: A,
) -> Result<Self, ProgramError> {
let input_account_info = {
let data = input_account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
let mut input_data_hash = H::hash(data.as_slice())
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
input_data_hash[0] = 0;
let tree_info = input_account_meta.get_tree_info();
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
}
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: None,
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
})
}
#[cfg(feature = "v2")]
pub fn new_read_only(
owner: &impl crate::PubkeyTrait,
input_account_meta: &CompressedAccountMetaReadOnly,
input_account: A,
packed_account_pubkeys: &[Pubkey],
) -> Result<Self, ProgramError> {
let data = input_account
.try_to_vec()
.map_err(|_| LightSdkError::Borsh)
.map_err(ProgramError::from)?;
let mut input_data_hash = H::hash(data.as_slice())
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
input_data_hash[0] = 0;
let tree_info = input_account_meta.get_tree_info();
let input_account_info = InAccountInfo {
data_hash: input_data_hash,
lamports: 0, merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
};
let account_hash = {
use light_compressed_account::compressed_account::{
CompressedAccount, CompressedAccountData,
};
let compressed_account = CompressedAccount {
address: Some(input_account_meta.address),
owner: owner.to_array().into(),
data: Some(CompressedAccountData {
data: vec![], data_hash: input_data_hash, discriminator: A::LIGHT_DISCRIMINATOR,
}),
lamports: 0,
};
let merkle_tree_pubkey = packed_account_pubkeys
.get(tree_info.merkle_tree_pubkey_index as usize)
.ok_or(LightSdkError::InvalidMerkleTreeIndex)
.map_err(ProgramError::from)?
.to_bytes()
.into();
compressed_account
.hash(&merkle_tree_pubkey, &tree_info.leaf_index, true)
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?
};
Ok(Self {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: Some(input_account_meta.address),
input: Some(input_account_info),
output: None,
},
should_remove_data: false,
read_only_account_hash: Some(account_hash),
_hasher: PhantomData,
})
}
pub fn to_account_info(mut self) -> Result<CompressedAccountInfo, ProgramError> {
if self.read_only_account_hash.is_some() {
return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into());
}
if let Some(output) = self.account_info.output.as_mut() {
if self.should_remove_data {
if !output.data.is_empty() {
return Err(LightSdkError::ExpectedNoData.into());
}
output.data_hash = DEFAULT_DATA_HASH;
output.discriminator = [0u8; 8];
} else {
output.data = self
.account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
output.data_hash = H::hash(output.data.as_slice())
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
output.data_hash[0] = 0;
}
}
Ok(self.account_info)
}
#[cfg(feature = "v2")]
pub fn to_packed_read_only_account(
self,
) -> Result<
light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount,
ProgramError,
> {
let account_hash = self
.read_only_account_hash
.ok_or(LightSdkError::NotReadOnlyAccount)?;
let input_account = self
.account_info
.input
.ok_or(ProgramError::InvalidAccountData)?;
use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount;
Ok(PackedReadOnlyCompressedAccount {
root_index: input_account.root_index,
merkle_context: input_account.merkle_context,
account_hash,
})
}
pub fn to_in_account(&self) -> Option<InAccount> {
self.account_info
.input
.as_ref()
.map(|input| input.into_in_account(self.account_info.address))
}
pub fn to_output_compressed_account_with_packed_context(
&self,
owner: Option<solana_pubkey::Pubkey>,
) -> Result<Option<OutputCompressedAccountWithPackedContext>, ProgramError> {
let owner = if let Some(owner) = owner {
owner.to_bytes().into()
} else {
self.owner.to_bytes().into()
};
if let Some(mut output) = self.account_info.output.clone() {
if self.should_remove_data {
if !output.data.is_empty() {
return Err(LightSdkError::ExpectedNoData.into());
}
output.data_hash = DEFAULT_DATA_HASH;
output.discriminator = [0u8; 8];
} else {
output.data = self
.account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
output.data_hash = H::hash(output.data.as_slice())
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
output.data_hash[0] = 0;
output.data = self
.account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
}
let result = OutputCompressedAccountWithPackedContext::from_with_owner(
&output,
owner,
self.account_info.address,
);
Ok(Some(result))
} else {
Ok(None)
}
}
pub(crate) fn new_mut_inner(
owner: &impl crate::PubkeyTrait,
input_account_meta: &impl CompressedAccountMetaTrait,
input_account: A,
) -> Result<(LightAccountInner<H, A, true>, Vec<u8>), ProgramError> {
let (input_account_info, data) = {
let data = input_account
.try_to_vec()
.map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
let mut input_data_hash = H::hash(data.as_slice())
.map_err(LightSdkError::from)
.map_err(ProgramError::from)?;
input_data_hash[0] = 0;
let tree_info = input_account_meta.get_tree_info();
(
InAccountInfo {
data_hash: input_data_hash,
lamports: input_account_meta.get_lamports().unwrap_or_default(),
merkle_context: PackedMerkleContext {
merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
queue_pubkey_index: tree_info.queue_pubkey_index,
leaf_index: tree_info.leaf_index,
prove_by_index: tree_info.prove_by_index,
},
root_index: input_account_meta.get_root_index().unwrap_or_default(),
discriminator: A::LIGHT_DISCRIMINATOR,
},
data,
)
};
let output_account_info = {
let output_merkle_tree_index = input_account_meta
.get_output_state_tree_index()
.ok_or(LightSdkError::OutputStateTreeIndexIsNone)
.map_err(ProgramError::from)?;
OutAccountInfo {
lamports: input_account_meta.get_lamports().unwrap_or_default(),
output_merkle_tree_index,
discriminator: A::LIGHT_DISCRIMINATOR,
..Default::default()
}
};
Ok((
LightAccountInner {
owner: owner.to_solana_pubkey(),
account: input_account,
account_info: CompressedAccountInfo {
address: input_account_meta.get_address(),
input: Some(input_account_info),
output: Some(output_account_info),
},
should_remove_data: false,
read_only_account_hash: None,
_hasher: PhantomData,
},
data,
))
}
}
}