diff --git a/src/solana/parser.rs b/src/solana/parser.rs index 2f02992..de5b440 100644 --- a/src/solana/parser.rs +++ b/src/solana/parser.rs @@ -10,7 +10,7 @@ use solana_sdk::{ pubkey::Pubkey, system_instruction::SystemInstruction, }; -use super::structs::{SolTransfer, SolanaAccount, SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, SolanaParseResponse, SolanaParsedTransaction, SolanaParsedTransactionPayload, SolanaSingleAddressTableLookup, SplTransfer}; +use super::structs::{AccountAddress, SolTransfer, SolanaAccount, SolanaAddressTableLookup, SolanaInstruction, SolanaMetadata, SolanaParseResponse, SolanaParsedTransaction, SolanaParsedTransactionPayload, SolanaSingleAddressTableLookup, SplTransfer}; // Length of a solana signature in bytes (64 bytes long) pub const LEN_SOL_SIGNATURE_BYTES: usize = 64; @@ -589,12 +589,19 @@ impl SolanaTransaction { let mut transfers: Vec = vec![]; let mut spl_transfers: Vec = vec![]; for i in self.message.instructions() { - let mut accounts: Vec = vec![]; - let mut address_table_lookups: Vec = vec![]; + // all_transaction_addresses contains all addresses (both static and table lookups) for the transaction + let mut all_transaction_addresses: Vec = vec![]; + + // The arrays below separately parse the different types of transaction accounts into two separate arrays -- either static or table lookups + let mut static_accounts: Vec = vec![]; + let mut atlu_addresses: Vec = vec![]; for a in i.accounts.clone() { // if the index is out of bounds of the static account keys array it is an address lookup table (only for versioned transactions) if a as usize >= self.message.static_account_keys().len() { - address_table_lookups.push(self.resolve_address_table_lookup(a as usize)?); + let atlu = self.resolve_address_table_lookup(a as usize)?; + // push the parsed address table lookup to both the lookups array AND the combined all transaction address array + atlu_addresses.push(atlu.clone()); + all_transaction_addresses.push(AccountAddress::AddressTableLookUp(atlu.clone())); continue; } let account_key = self @@ -608,7 +615,9 @@ impl SolanaTransaction { signer: self.message.is_signer(a as usize), writable: self.message.is_maybe_writable(a as usize, None), }; - accounts.push(acct); + // push the parsed static account to both the static account array AND the combined all transaction address array + static_accounts.push(acct.clone()); + all_transaction_addresses.push(AccountAddress::Static(acct.clone())); } let program_key = i.program_id(self.message.static_account_keys()).to_string(); match program_key.as_str() { @@ -616,20 +625,20 @@ impl SolanaTransaction { let system_instruction: SystemInstruction = bincode::deserialize(&i.data) .map_err(|_| "Could not parse system instruction")?; if let SystemInstruction::Transfer { lamports } = system_instruction { - if accounts.len() != 2 { + if all_transaction_addresses.len() != 2 { return Err("System Program Transfer Instruction should have exactly 2 arguments".into()) } let transfer = SolTransfer { amount: lamports.to_string(), - to: accounts[1].account_key.clone(), - from: accounts[0].account_key.clone(), + from: all_transaction_addresses[0].to_string(), // the "from" address is the address at index 0 in the address parameter array in a Sol transfer + to: all_transaction_addresses[1].to_string(), // the "to" address is the address at index 1 in the address parameter array in a Sol transfer }; transfers.push(transfer); } } TOKEN_PROGRAM_KEY => { let token_program_instruction: SplInstructionData = SplInstructionData::parse_spl_transfer_data(&i.data)?; - let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_instruction, accounts.clone())?; + let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_instruction, all_transaction_addresses.clone())?; match spl_tranfer_opt { Some(spl_transfer) => spl_transfers.push(spl_transfer), None => (), @@ -637,7 +646,7 @@ impl SolanaTransaction { } TOKEN_2022_PROGRAM_KEY => { let token_program_22_instruction: SplInstructionData = SplInstructionData::parse_spl_transfer_data(&i.data)?; - let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_22_instruction, accounts.clone())?; + let spl_tranfer_opt = self.parse_spl_instruction_data(token_program_22_instruction, all_transaction_addresses.clone())?; match spl_tranfer_opt { Some(spl_transfer) => spl_transfers.push(spl_transfer), None => (), @@ -648,9 +657,9 @@ impl SolanaTransaction { let instruction_data_hex: String = hex::encode(&i.data); let inst = SolanaInstruction { program_key, - accounts, + accounts: static_accounts, instruction_data_hex, - address_table_lookups, + address_table_lookups: atlu_addresses, }; instructions.push(inst); } @@ -658,14 +667,14 @@ impl SolanaTransaction { } // Parse Instruction to Solana Token Program OR Solana Token Program 2022 and return something if it is an SPL transfer - fn parse_spl_instruction_data(&self, token_instruction: SplInstructionData, accounts: Vec) -> Result, Box> { + fn parse_spl_instruction_data(&self, token_instruction: SplInstructionData, all_transaction_addresses: Vec) -> Result, Box> { if let SplInstructionData::Transfer { amount } = token_instruction { - let signers = self.get_spl_multisig_signers_if_exist(&accounts, 3)?; + let signers = self.get_spl_multisig_signers_if_exist(&all_transaction_addresses, 3)?; let spl_transfer = SplTransfer { amount: amount.to_string(), - to: accounts[1].account_key.clone(), - from: accounts[0].account_key.clone(), - owner: accounts[2].account_key.clone(), + from: all_transaction_addresses[0].to_string(), // the "from" address is the address at index 0 in the address parameter array in an SPL Transfer Instruction + to: all_transaction_addresses[1].to_string(), // the "to" address is the address at index 1 in the address parameter array in an SPL Transfer Instruction + owner: all_transaction_addresses[2].to_string(), // the "owner" address is the address at index 2 in the address parameter array in an SPL Transfer Instruction signers, decimals: None, fee: None, @@ -673,27 +682,27 @@ impl SolanaTransaction { }; return Ok(Some(spl_transfer)) } else if let SplInstructionData::TransferChecked{ amount, decimals } = token_instruction { - let signers = self.get_spl_multisig_signers_if_exist(&accounts, 4)?; + let signers = self.get_spl_multisig_signers_if_exist(&all_transaction_addresses, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), - to: accounts[2].account_key.clone(), - from: accounts[0].account_key.clone(), - token_mint: Some(accounts[1].account_key.clone()), - owner: accounts[3].account_key.clone(), + from: all_transaction_addresses[0].to_string(), // the "from" address is the address at index 0 in the address parameter array in an SPL TransferChecked Instruction + token_mint: Some(all_transaction_addresses[1].to_string()), // the "token_mint" address is the address at index 1 in the address parameter array in a SPL TransferChecked Instruction + to: all_transaction_addresses[2].to_string(), // the "to" address is the address at index 2 in the address parameter array in a SPL TransferChecked Instruction + owner: all_transaction_addresses[3].to_string(), // the "owner" address is the address at index 3 in the address parameter array in a SPL TransferChecked Instruction signers, decimals: Some(decimals.to_string()), fee: None, }; return Ok(Some(spl_transfer)) } else if let SplInstructionData::TransferCheckedWithFee { amount, decimals, fee } = token_instruction { - let signers = self.get_spl_multisig_signers_if_exist(&accounts, 4)?; + let signers = self.get_spl_multisig_signers_if_exist(&all_transaction_addresses, 4)?; let spl_transfer = SplTransfer { amount: amount.to_string(), - to: accounts[2].account_key.clone(), - from: accounts[0].account_key.clone(), - token_mint: Some(accounts[1].account_key.clone()), + from: all_transaction_addresses[0].to_string(), // the "from" address is the address at index 0 in the address parameter array in a SPL TransferCheckedWithFee Instruction + token_mint: Some(all_transaction_addresses[1].to_string()), // the "token_mint" address is the address at index 1 in the address parameter array in a SPL TransferCheckedWithFee Instruction + to: all_transaction_addresses[2].to_string(), // the "to" address is the address at index 2 in the address parameter array in a SPL TransferCheckedWithFee Instruction + owner: all_transaction_addresses[3].to_string(), // the "owner" address is the address at index 3 in the address parameter array in a SPL TransferCheckedWithFee Instruction signers, - owner: accounts[3].account_key.clone(), decimals: Some(decimals.to_string()), fee: Some(fee.to_string()), }; @@ -702,11 +711,11 @@ impl SolanaTransaction { return Ok(None) } - fn get_spl_multisig_signers_if_exist(&self, accounts: &Vec, num_accts_before_signer: usize) -> Result, Box> { - if accounts.len() < num_accts_before_signer { + fn get_spl_multisig_signers_if_exist(&self, all_transaction_addresses: &Vec, num_accts_before_signer: usize) -> Result, Box> { + if all_transaction_addresses.len() < num_accts_before_signer { return Err(format!("Invalid number of accounts provided for spl token transfer instruction").into()) } - Ok(accounts[num_accts_before_signer..accounts.len()].to_vec().into_iter().map(|a| a.account_key).collect()) + Ok(all_transaction_addresses[num_accts_before_signer..all_transaction_addresses.len()].to_vec().into_iter().map(|a| a.to_string()).collect()) } fn recent_blockhash(&self) -> String { diff --git a/src/solana/structs.rs b/src/solana/structs.rs index 8bbef08..f4a897f 100644 --- a/src/solana/structs.rs +++ b/src/solana/structs.rs @@ -1,3 +1,5 @@ +use std::fmt; + #[derive(Debug, Clone, PartialEq)] pub struct SolanaMetadata { pub signatures: Vec, @@ -73,3 +75,24 @@ pub struct SolanaParsedTransaction { pub struct SolanaParseResponse { pub solana_parsed_transaction: SolanaParsedTransaction, } + +#[derive(Debug, Clone, PartialEq)] +pub enum AccountAddress { + /// Static Account Addresses refer to addresses whose string/hex representations have been entirely included in the serialized transaction + Static(SolanaAccount), + /// `AddressTableLookUp` Addresses refer to addresses whose string/hex representation have NOT been included + /// Rather, only a reference has been included using the concept of Address Lookup Tables -- + /// NOTE the difference between Address Lookup Tables and Address Table Lookups + /// Address Lookup Tables -- the on chain tables where addresses are stored + /// Address Table Lookups -- the struct that gets serialized with transactions that is used to POINT to an address in a lookup table --> + AddressTableLookUp(SolanaSingleAddressTableLookup), +} + +impl fmt::Display for AccountAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AccountAddress::Static(account) => write!(f, "{}", account.account_key), + AccountAddress::AddressTableLookUp(_) => write!(f, "ADDRESS_TABLE_LOOKUP"), + } + } +} \ No newline at end of file diff --git a/src/solana/tests.rs b/src/solana/tests.rs index b576382..df1ce2a 100644 --- a/src/solana/tests.rs +++ b/src/solana/tests.rs @@ -892,3 +892,198 @@ use crate::solana::parser::{SolanaTransaction, TOKEN_PROGRAM_KEY, TOKEN_2022_PRO // Test Program called in the instruction assert_eq!(tx_metadata.instructions[0].program_key, TOKEN_2022_PROGRAM_KEY) } + + #[test] + fn parse_spl_transfer_using_address_table_lookups() { + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let _ = parsed_tx.transaction_metadata().unwrap(); + } + + #[test] + fn parse_spl_transfer_using_address_table_lookups_mint() { + // This transaction contains two SPL transfer instructions + // BOTH Spl transfer instructions use an Address Table look up to represent the Token Mint address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04021303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // EMPTY BECAUSE OF ATLU + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup token mint address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!(spl_transfer_2.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // EMPTY BECAUSE OF ATLU + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + } + + #[test] + fn parse_spl_transfer_using_address_table_lookups_recipient() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Recipient address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020313000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for receiving "to" address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!(spl_transfer_2.to, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_2.token_mint, + Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) + ); + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + } + + #[test] + fn parse_spl_transfer_using_address_table_lookups_sender() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Sending address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04130303000a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for sending "from" address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!(spl_transfer_2.from, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!( + spl_transfer_2.token_mint, + Some("EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string()) + ); + assert_eq!( + spl_transfer_2.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + } + + #[test] + fn parse_spl_transfer_using_address_table_lookups_owner() { + // This transaction contains two SPL transfer instructions + // 1. Uses an Address Table look up to represent the Token Mint address + // 2. Uses an Address Table look up to represent the Owner address + // The parser will return the flag ADDRESS_TABLE_LOOKUP for these non statically included addresses + + // ensure that transaction gets parsed without errors + let unsigned_transaction = "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008001000b10b9334994c55889c1e129158c59a9b3b16fd9bfc9bedd105a8e1d7b7a8644110772f445b3a19ac048d2a928fe0774cf7b8b5efa7c6457cbccbc82ecf0eac93c792343cde9faec81dfd6963f83ea57e8075f2db9eb0c461d195737e143f9b16909c52568e818f6871d033a00dba9ae878df8ba008104e34fb0332d685f3eacdf6a5149b5337cf8079ab25763ae8e8f95a9b09d2325dcc2ee5f8e8640b7eacf470d283d0dd282354fef0ae3b0e227d37cd89ca266fb17ddf8f7cb7ccefbe4ebdc5506a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b2100000000d1a3910dca452ccc0c6d513e570b0a5cee7edf44fa74e1410cd405fba63e96100306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a400000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8591e8c4fab8994494c8f1e5c1287445b2917d60c43c79aa959162f5d6000598d32000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92ccd355fe72bcf08d5ee763f52bb9603e025ef8e1d0340f28a576313251507310479d55bf231c06eee74c56ece681507fdb1b2dea3f48e5102b1cda256bc138fb43ffa27f5d7f64a74c09b1f295879de4b09ab36dfc9dd514b321aa7b38ce5e8ee501f6575c6376b0fc00c38a8f474ed66466d3cc3bf159e8d2be46427a83a9c0a08000903a8d002000000000008000502e7e1060005020607090022bb6ad79d0c1600090600010a130b0c01010c04021301000a0c9c0100000000000006090600030d130b0c01010c04020003130a0c4603000000000000060906000400140b0c01010e120c0002040e140e0f0e150010111204020c1624e517cb977ae3ad2a010000003d016400013e9c070000000000c6c53a0000000000e803000c030400000109015de6c0e5b44625227af5ec45b683057e191d6d7bf7ff43e3d25f31d5d5e81dac03b86fba04c013b970".to_string(); + let parsed_tx = SolanaTransaction::new(&unsigned_transaction, true).unwrap(); + let tx_metadata = parsed_tx.transaction_metadata().unwrap(); + + let spl_transfers = tx_metadata.spl_transfers; + + // SPL transfer 1 (Uses an address table lookup token mint address) + let spl_transfer_1 = spl_transfers[0].clone(); + assert_eq!( + spl_transfer_1.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_1.to, + "8jjWmLhYdqrtFMcEkiMDdqkEN85cvEFnNS4LFgNf5NRv".to_string() + ); + assert_eq!(spl_transfer_1.token_mint, Some("ADDRESS_TABLE_LOOKUP".to_string())); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + assert_eq!( + spl_transfer_1.owner, + "DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string() + ); + + // SPL transfer 2 (Uses an address table lookup for owner address) + let spl_transfer_2 = spl_transfers[1].clone(); + assert_eq!( + spl_transfer_2.from, + "3NfEggXMdHJPTYV4pkbHjh4iC3q5NoLkXTwyWAB1QSkp".to_string() + ); + assert_eq!( + spl_transfer_2.to, + "EGaQJtKJ4zctDY4VjP98f2KDtEeeso8JGk5a4E8X1EaV".to_string() + ); + assert_eq!( + spl_transfer_2.token_mint, + Some("DTwnQq6QdYRibHtyzWM5MxqsBuDTiUD8aeaFcjesnoKt".to_string()) + ); + assert_eq!(spl_transfer_2.owner, "ADDRESS_TABLE_LOOKUP".to_string()); // Shows the flag ADDRESS_TABLE_LOOKUP because an ATLU is used for this address in the transaction + } \ No newline at end of file