From 32fea0496ec7b7a4eb406af4f2fd6d7d640339f6 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Fri, 24 Jul 2020 17:45:21 -0600 Subject: [PATCH] Add token account decoding (#11136) * Add token decoding * Bump versions --- Cargo.lock | 70 +++++++-- account-decoder/Cargo.toml | 4 +- account-decoder/src/lib.rs | 1 + account-decoder/src/parse_account_data.rs | 9 +- account-decoder/src/parse_token.rs | 171 ++++++++++++++++++++++ transaction-status/Cargo.toml | 2 +- 6 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 account-decoder/src/parse_token.rs diff --git a/Cargo.lock b/Cargo.lock index e0e328ca6..143be1cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,24 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cbindgen" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "104ca409bbff8293739438c71820a2606111b5f8f81835536dc673dfd807369e" +dependencies = [ + "clap", + "heck", + "log 0.4.8", + "proc-macro2 1.0.17", + "quote 1.0.6", + "serde", + "serde_json", + "syn 1.0.27", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.0.49" @@ -1101,6 +1119,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.13" @@ -2470,9 +2497,9 @@ checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +checksum = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24" dependencies = [ "winapi 0.3.8", ] @@ -2912,9 +2939,11 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-sdk 1.2.13", "solana-sdk 1.3.0", "solana-vote-program", "spl-memo", + "spl-token", "thiserror", ] @@ -3812,9 +3841,9 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "1.2.10" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3869d4a4784d10fc0db91fc666a7ccb916fd47e74636e37c328df5a3b7f8157f" +checksum = "cae779f912f1cbe2fe91df656ecc5ccdda9dd3378c0934f4390b7ba33501b3dc" dependencies = [ "bincode", "bs58", @@ -3826,11 +3855,13 @@ dependencies = [ "num-derive 0.3.0", "num-traits", "pbkdf2", + "rustc_version", + "rustversion", "serde", "serde_bytes", "serde_derive", "sha2", - "solana-sdk-macro 1.2.10", + "solana-sdk-macro 1.2.13", "thiserror", ] @@ -3873,13 +3904,14 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.2.10" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fba494b48ee7d1b1c9cfd2df292d9dad69ccfd1f2b107eb030e29601cc4103" +checksum = "8924af69f2685287988a530014c3268d78d6ce3fe4a1a61f87537c545afa8427" dependencies = [ "bs58", "proc-macro2 1.0.17", "quote 1.0.6", + "rustversion", "syn 1.0.27", ] @@ -4203,11 +4235,25 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spl-memo" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451acd972c0ed2d114c42f8fffdd9a6007840e51fe7791f250d183e72c8ff7b5" +dependencies = [ + "solana-sdk 1.2.13", +] + +[[package]] +name = "spl-token" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d28cb8e0db4c8d578cde21a416af3e376ce4c3eeb6f5bcd7025fdf8980a3ce8" +checksum = "a21aa023866f97c2644f932c2562d3c20e23a817e8abb0702625e0601cf9af80" dependencies = [ - "solana-sdk 1.2.10", + "cbindgen", + "num-derive 0.2.5", + "num-traits", + "remove_dir_all", + "solana-sdk 1.2.13", + "thiserror", ] [[package]] @@ -4797,6 +4843,12 @@ dependencies = [ "smallvec 1.4.0", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + [[package]] name = "unicode-width" version = "0.1.7" diff --git a/account-decoder/Cargo.toml b/account-decoder/Cargo.toml index 96e13e390..4c4633c26 100644 --- a/account-decoder/Cargo.toml +++ b/account-decoder/Cargo.toml @@ -15,7 +15,9 @@ Inflector = "0.11.4" lazy_static = "1.4.0" solana-sdk = { path = "../sdk", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } -spl-memo = "1.0.2" +spl-memo = { version = "1.0.4", features = ["skip-no-mangle"] } +spl-sdk = { package = "solana-sdk", version = "=1.2.13", default-features = false } +spl-token = { version = "1.0.2", features = ["skip-no-mangle"] } serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56" diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs index ec2d52f30..45afe8adf 100644 --- a/account-decoder/src/lib.rs +++ b/account-decoder/src/lib.rs @@ -5,6 +5,7 @@ extern crate serde_derive; pub mod parse_account_data; pub mod parse_nonce; +pub mod parse_token; pub mod parse_vote; use crate::parse_account_data::parse_account_data; diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index 2e46d8e0b..cde8500bd 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -1,4 +1,4 @@ -use crate::{parse_nonce::parse_nonce, parse_vote::parse_vote}; +use crate::{parse_nonce::parse_nonce, parse_token::parse_token, parse_vote::parse_vote}; use inflector::Inflector; use serde_json::{json, Value}; use solana_sdk::{instruction::InstructionError, pubkey::Pubkey, system_program}; @@ -8,11 +8,13 @@ use thiserror::Error; lazy_static! { static ref SYSTEM_PROGRAM_ID: Pubkey = Pubkey::from_str(&system_program::id().to_string()).unwrap(); + static ref TOKEN_PROGRAM_ID: Pubkey = Pubkey::from_str(&spl_token::id().to_string()).unwrap(); static ref VOTE_PROGRAM_ID: Pubkey = Pubkey::from_str(&solana_vote_program::id().to_string()).unwrap(); pub static ref PARSABLE_PROGRAM_IDS: HashMap = { let mut m = HashMap::new(); m.insert(*SYSTEM_PROGRAM_ID, ParsableAccount::Nonce); + m.insert(*TOKEN_PROGRAM_ID, ParsableAccount::Token); m.insert(*VOTE_PROGRAM_ID, ParsableAccount::Vote); m }; @@ -20,6 +22,9 @@ lazy_static! { #[derive(Error, Debug)] pub enum ParseAccountError { + #[error("{0:?} account not parsable")] + AccountNotParsable(ParsableAccount), + #[error("Program not parsable")] ProgramNotParsable, @@ -34,6 +39,7 @@ pub enum ParseAccountError { #[serde(rename_all = "camelCase")] pub enum ParsableAccount { Nonce, + Token, Vote, } @@ -43,6 +49,7 @@ pub fn parse_account_data(program_id: &Pubkey, data: &[u8]) -> Result serde_json::to_value(parse_nonce(data)?)?, + ParsableAccount::Token => serde_json::to_value(parse_token(data)?)?, ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?, }; Ok(json!({ diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs new file mode 100644 index 000000000..bb2170d8d --- /dev/null +++ b/account-decoder/src/parse_token.rs @@ -0,0 +1,171 @@ +use crate::parse_account_data::{ParsableAccount, ParseAccountError}; +use spl_sdk::pubkey::Pubkey; +use spl_token::{ + option::COption, + state::{Account, Mint, Multisig, State}, +}; +use std::mem::size_of; + +pub fn parse_token(data: &[u8]) -> Result { + let mut data = data.to_vec(); + if data.len() == size_of::() { + let account: Account = *State::unpack(&mut data) + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + Ok(TokenAccountType::Account(UiTokenAccount { + mint: account.mint.to_string(), + owner: account.owner.to_string(), + amount: account.amount, + delegate: match account.delegate { + COption::Some(pubkey) => Some(pubkey.to_string()), + COption::None => None, + }, + is_initialized: account.is_initialized, + is_native: account.is_native, + delegated_amount: account.delegated_amount, + })) + } else if data.len() == size_of::() { + let mint: Mint = *State::unpack(&mut data) + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + Ok(TokenAccountType::Mint(UiMint { + owner: match mint.owner { + COption::Some(pubkey) => Some(pubkey.to_string()), + COption::None => None, + }, + decimals: mint.decimals, + is_initialized: mint.is_initialized, + })) + } else if data.len() == size_of::() { + let multisig: Multisig = *State::unpack(&mut data) + .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::Token))?; + Ok(TokenAccountType::Multisig(UiMultisig { + num_required_signers: multisig.m, + num_valid_signers: multisig.n, + is_initialized: multisig.is_initialized, + signers: multisig + .signers + .iter() + .filter_map(|pubkey| { + if pubkey != &Pubkey::default() { + Some(pubkey.to_string()) + } else { + None + } + }) + .collect(), + })) + } else { + Err(ParseAccountError::AccountNotParsable( + ParsableAccount::Token, + )) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum TokenAccountType { + Account(UiTokenAccount), + Mint(UiMint), + Multisig(UiMultisig), +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiTokenAccount { + pub mint: String, + pub owner: String, + pub amount: u64, + pub delegate: Option, + pub is_initialized: bool, + pub is_native: bool, + pub delegated_amount: u64, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiMint { + pub owner: Option, + pub decimals: u8, + pub is_initialized: bool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiMultisig { + pub num_required_signers: u8, + pub num_valid_signers: u8, + pub is_initialized: bool, + pub signers: Vec, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_token() { + let mint_pubkey = Pubkey::new(&[2; 32]); + let owner_pubkey = Pubkey::new(&[3; 32]); + let mut account_data = [0; size_of::()]; + let mut account: &mut Account = State::unpack_unchecked(&mut account_data).unwrap(); + account.mint = mint_pubkey; + account.owner = owner_pubkey; + account.amount = 42; + account.is_initialized = true; + assert_eq!( + parse_token(&account_data).unwrap(), + TokenAccountType::Account(UiTokenAccount { + mint: mint_pubkey.to_string(), + owner: owner_pubkey.to_string(), + amount: 42, + delegate: None, + is_initialized: true, + is_native: false, + delegated_amount: 0, + }), + ); + + let mut mint_data = [0; size_of::()]; + let mut mint: &mut Mint = State::unpack_unchecked(&mut mint_data).unwrap(); + mint.owner = COption::Some(owner_pubkey); + mint.decimals = 3; + mint.is_initialized = true; + assert_eq!( + parse_token(&mint_data).unwrap(), + TokenAccountType::Mint(UiMint { + owner: Some(owner_pubkey.to_string()), + decimals: 3, + is_initialized: true, + }), + ); + + let signer1 = Pubkey::new(&[1; 32]); + let signer2 = Pubkey::new(&[2; 32]); + let signer3 = Pubkey::new(&[3; 32]); + let mut multisig_data = [0; size_of::()]; + let mut multisig: &mut Multisig = State::unpack_unchecked(&mut multisig_data).unwrap(); + let mut signers = [Pubkey::default(); 11]; + signers[0] = signer1; + signers[1] = signer2; + signers[2] = signer3; + multisig.m = 2; + multisig.n = 3; + multisig.is_initialized = true; + multisig.signers = signers; + assert_eq!( + parse_token(&multisig_data).unwrap(), + TokenAccountType::Multisig(UiMultisig { + num_required_signers: 2, + num_valid_signers: 3, + is_initialized: true, + signers: vec![ + signer1.to_string(), + signer2.to_string(), + signer3.to_string() + ], + }), + ); + + let bad_data = vec![0; 4]; + assert!(parse_token(&bad_data).is_err()); + } +} diff --git a/transaction-status/Cargo.toml b/transaction-status/Cargo.toml index f7d0d7457..638b5c812 100644 --- a/transaction-status/Cargo.toml +++ b/transaction-status/Cargo.toml @@ -16,7 +16,7 @@ lazy_static = "1.4.0" solana-sdk = { path = "../sdk", version = "1.3.0" } solana-stake-program = { path = "../programs/stake", version = "1.3.0" } solana-vote-program = { path = "../programs/vote", version = "1.3.0" } -spl-memo = "1.0.2" +spl-memo = { version = "1.0.4", features = ["skip-no-mangle"] } serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56"