Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: anchor account subcommand to read program account #1923

Merged
merged 20 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/no-cashing-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ jobs:
path: tests/relations-derivation
- cmd: cd tests/anchor-cli-idl && ./test.sh
path: tests/anchor-cli-idl
- cmd: cd tests/anchor-cli-account && anchor test --skip-lint
path: tests/anchor-cli-account
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup/
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ jobs:
path: tests/relations-derivation
- cmd: cd tests/anchor-cli-idl && ./test.sh
path: tests/anchor-cli-idl
- cmd: cd tests/anchor-cli-account && anchor test --skip-lint
path: tests/anchor-cli-account
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Allow custom cluster config ([#2271](https://github.com/coral-xyz/anchor/pull/2271)).
- ts: Add optional flag to parseLogs to throw an error on decoding failure ([#2043](https://github.com/coral-xyz/anchor/pull/2043)).
- cli: Add `test.validator.geyser_plugin_config` support ([#2016](https://github.com/coral-xyz/anchor/pull/2016)).
- cli: Add `account` subcommand to cli ([#1923](https://github.com/project-serum/anchor/pull/1923))

### Fixes

Expand Down
248 changes: 247 additions & 1 deletion cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::config::{
use anchor_client::Cluster;
use anchor_lang::idl::{IdlAccount, IdlInstruction, ERASED_AUTHORITY};
use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize};
use anchor_syn::idl::Idl;
use anchor_syn::idl::{EnumFields, Idl, IdlType, IdlTypeDefinitionTy};
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use flate2::read::GzDecoder;
Expand All @@ -17,6 +17,7 @@ use reqwest::blocking::multipart::{Form, Part};
use reqwest::blocking::Client;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value as JsonValue};
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_program::instruction::{AccountMeta, Instruction};
Expand Down Expand Up @@ -267,6 +268,16 @@ pub enum Command {
#[clap(required = false, last = true)]
cargo_args: Vec<String>,
},
/// Fetch and deserialize an account using the IDL provided.
Account {
/// Account struct to deserialize
account_type: String,
/// Address of the account to deserialize
address: Pubkey,
/// IDL to use (defaults to workspace IDL)
#[clap(long)]
idl: Option<String>,
},
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -471,6 +482,11 @@ pub fn entry(opts: Opts) -> Result<()> {
skip_lint,
cargo_args,
),
Command::Account {
account_type,
address,
idl,
} => account(&opts.cfg_override, account_type, address, idl),
}
}

Expand Down Expand Up @@ -1854,6 +1870,236 @@ fn write_idl(idl: &Idl, out: OutFile) -> Result<()> {
Ok(())
}

fn account(
cfg_override: &ConfigOverride,
account_type: String,
address: Pubkey,
idl_filepath: Option<String>,
) -> Result<()> {
let (program_name, account_type_name) = account_type
.split_once('.') // Split at first occurance of dot
.and_then(|(x, y)| y.find('.').map_or_else(|| Some((x, y)), |_| None)) // ensures no dots in second substring
.ok_or_else(|| {
anyhow!(
"Please enter the account struct in the following format: <program_name>.<Account>",
)
})?;

let idl = idl_filepath.map_or_else(
|| {
Config::discover(cfg_override)
.expect("Error when detecting workspace.")
.expect("Not in workspace.")
.read_all_programs()
.expect("Workspace must contain atleast one program.")
.iter()
.find(|&p| p.lib_name == *program_name)
.unwrap_or_else(|| panic!("Program {} not found in workspace.", program_name))
.idl
.as_ref()
.expect("IDL not found. Please build the program atleast once to generate the IDL.")
.clone()
},
|idl_path| {
let bytes = fs::read(idl_path).expect("Unable to read IDL.");
let idl: Idl = serde_json::from_reader(&*bytes).expect("Invalid IDL format.");

if idl.name != program_name {
panic!("IDL does not match program {}.", program_name);
}

idl
},
);

let mut cluster = &Config::discover(cfg_override)
.map(|cfg| cfg.unwrap())
.map(|cfg| cfg.provider.cluster.clone())
.unwrap_or(Cluster::Localnet);
cluster = cfg_override.cluster.as_ref().unwrap_or(cluster);

let data = RpcClient::new(cluster.url()).get_account_data(&address)?;
if data.len() < 8 {
return Err(anyhow!(
"The account has less than 8 bytes and is not an Anchor account."
));
}
let mut data_view = &data[8..];

let deserialized_json =
deserialize_idl_struct_to_json(&idl, account_type_name, &mut data_view)?;

println!(
"{}",
serde_json::to_string_pretty(&deserialized_json).unwrap()
);

Ok(())
}

// Deserializes a user defined IDL struct/enum by munching the account data.
// Recursively deserializes elements one by one
fn deserialize_idl_struct_to_json(
idl: &Idl,
account_type_name: &str,
data: &mut &[u8],
) -> Result<JsonValue, anyhow::Error> {
let account_type = &idl
.accounts
.iter()
.chain(idl.types.iter())
.find(|account_type| account_type.name == account_type_name)
.ok_or_else(|| {
anyhow::anyhow!("Struct/Enum named {} not found in IDL.", account_type_name)
})?
.ty;

let mut deserialized_fields = Map::new();

match account_type {
IdlTypeDefinitionTy::Struct { fields } => {
for field in fields {
deserialized_fields.insert(
field.name.clone(),
deserialize_idl_type_to_json(&field.ty, data, idl)?,
);
}
}
IdlTypeDefinitionTy::Enum { variants } => {
let repr = <u8 as AnchorDeserialize>::deserialize(data)?;

let variant = variants
.get(repr as usize)
.unwrap_or_else(|| panic!("Error while deserializing enum variant {}", repr));

let mut value = json!({});

if let Some(enum_field) = &variant.fields {
match enum_field {
EnumFields::Named(fields) => {
let mut values = Map::new();

for field in fields {
values.insert(
field.name.clone(),
deserialize_idl_type_to_json(&field.ty, data, idl)?,
);
}

value = JsonValue::Object(values);
}
EnumFields::Tuple(fields) => {
let mut values = Vec::new();

for field in fields {
values.push(deserialize_idl_type_to_json(field, data, idl)?);
}

value = JsonValue::Array(values);
}
}
}

deserialized_fields.insert(variant.name.clone(), value);
}
}

Ok(JsonValue::Object(deserialized_fields))
}

// Deserializes a primitive type using AnchorDeserialize
fn deserialize_idl_type_to_json(
idl_type: &IdlType,
data: &mut &[u8],
parent_idl: &Idl,
) -> Result<JsonValue, anyhow::Error> {
if data.is_empty() {
return Err(anyhow::anyhow!("Unable to parse from empty bytes"));
}

Ok(match idl_type {
IdlType::Bool => json!(<bool as AnchorDeserialize>::deserialize(data)?),
IdlType::U8 => {
json!(<u8 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I8 => {
json!(<i8 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::U16 => {
json!(<u16 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I16 => {
json!(<i16 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::U32 => {
json!(<u32 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I32 => {
json!(<i32 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::F32 => json!(<f32 as AnchorDeserialize>::deserialize(data)?),
IdlType::U64 => {
json!(<u64 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::I64 => {
json!(<i64 as AnchorDeserialize>::deserialize(data)?)
}
IdlType::F64 => json!(<f64 as AnchorDeserialize>::deserialize(data)?),
IdlType::U128 => {
// TODO: Remove to_string once serde_json supports u128 deserialization
json!(<u128 as AnchorDeserialize>::deserialize(data)?.to_string())
}
IdlType::I128 => {
// TODO: Remove to_string once serde_json supports i128 deserialization
json!(<i128 as AnchorDeserialize>::deserialize(data)?.to_string())
}
IdlType::U256 => todo!("Upon completion of u256 IDL standard"),
IdlType::I256 => todo!("Upon completion of i256 IDL standard"),
IdlType::Bytes => JsonValue::Array(
<Vec<u8> as AnchorDeserialize>::deserialize(data)?
.iter()
.map(|i| json!(*i))
.collect(),
),
IdlType::String => json!(<String as AnchorDeserialize>::deserialize(data)?),
IdlType::PublicKey => {
json!(<Pubkey as AnchorDeserialize>::deserialize(data)?.to_string())
}
IdlType::Defined(type_name) => deserialize_idl_struct_to_json(parent_idl, type_name, data)?,
IdlType::Option(ty) => {
let is_present = <u8 as AnchorDeserialize>::deserialize(data)?;

if is_present == 0 {
JsonValue::String("None".to_string())
} else {
deserialize_idl_type_to_json(ty, data, parent_idl)?
}
}
IdlType::Vec(ty) => {
let size: usize = <u32 as AnchorDeserialize>::deserialize(data)?
.try_into()
.unwrap();

let mut vec_data: Vec<JsonValue> = Vec::with_capacity(size);

for _ in 0..size {
vec_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
}

JsonValue::Array(vec_data)
}
IdlType::Array(ty, size) => {
let mut array_data: Vec<JsonValue> = Vec::with_capacity(*size);

for _ in 0..*size {
array_data.push(deserialize_idl_type_to_json(ty, data, parent_idl)?);
}

JsonValue::Array(array_data)
}
})
}

enum OutFile {
Stdout,
File(PathBuf),
Expand Down
23 changes: 23 additions & 0 deletions docs/src/pages/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ FLAGS:
-V, --version Prints version information

SUBCOMMANDS:
account Fetch and deserialize an account using the IDL provided
build Builds the workspace
cluster Cluster commands
deploy Deploys each program in the workspace
Expand All @@ -37,6 +38,28 @@ SUBCOMMANDS:
Cargo.toml
```

## Account

```
anchor account <program-name>.<AccountTypeName> <account_pubkey>
```

Fetches an account with the given public key and deserializes the data to JSON using the type name provided. If this command is run from within a workspace, the workspace's IDL files will be used to get the data types. Otherwise, the path to the IDL file must be provided.

The `program-name` is the name of the program where the account struct resides, usually under `programs/<program-name>`. `program-name` should be provided in a case-sensitive manner exactly as the folder name, usually in kebab-case.

The `AccountTypeName` is the name of the account struct, usually in PascalCase.

The `account_pubkey` refers to the Pubkey of the account to deserialise, in Base58.

Example Usage: `anchor account anchor-escrow.EscrowAccount 3PNkzWKXCsbjijbasnx55NEpJe8DFXvEEbJKdRKpDcfK`, deserializes an account in the given pubkey with the account struct `EscrowAccount` defined in the `anchor-escrow` program.

```
anchor account <program-name>.<AccountTypeName> <account_pubkey> --idl <path/to/idl.json>
```

Deserializes the account with the data types provided in the given IDL file even if inside a workspace.

## Build

```shell
Expand Down
2 changes: 2 additions & 0 deletions lang/syn/src/idl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ impl std::str::FromStr for IdlType {
"f64" => IdlType::F64,
"u128" => IdlType::U128,
"i128" => IdlType::I128,
"u256" => IdlType::U256,
"i256" => IdlType::I256,
"Vec<u8>" => IdlType::Bytes,
"String" | "&str" | "&'staticstr" => IdlType::String,
"Pubkey" => IdlType::PublicKey,
Expand Down
14 changes: 14 additions & 0 deletions tests/anchor-cli-account/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[programs.localnet]
account_command = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"

[registry]
url = "https://anchor.projectserum.com"

[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

[features]
7 changes: 7 additions & 0 deletions tests/anchor-cli-account/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[profile.release]
overflow-checks = true

[workspace]
members = [
"programs/*"
]
19 changes: 19 additions & 0 deletions tests/anchor-cli-account/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "anchor-cli-account",
"version": "0.25.0",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=11"
},
"scripts": {
"test": "anchor test"
}
}