cli: `anchor account` subcommand to read program account (#1923)
* Setup account subcommand skeleton * Move IDL deserialization code to syn module * Change HashMap to serde_json * Add enum deserialization * Add account subcommand to docs * Fix lint * Add validation for account type * Fix solana-sdk dependency version * Fix clippy warnings * Move IDL deserialization code to cli module * Remove debug print * Add integration tests * Update documentation with example * Fix clippy warnings * Fix leftover merge conflict * run prettier Co-authored-by: Henry-E <henry.elder@adaptcentre.ie> Co-authored-by: henrye <henry@notanemail>
This commit is contained in:
parent
03eff348db
commit
b662ff1460
|
@ -305,6 +305,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/
|
||||
|
|
|
@ -417,6 +417,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/
|
||||
|
|
|
@ -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))
|
||||
- cli: Add `ticks_per_slot` option to Validator args ([#1875](https://github.com/coral-xyz/anchor/pull/1875)).
|
||||
|
||||
### Fixes
|
||||
|
|
248
cli/src/lib.rs
248
cli/src/lib.rs
|
@ -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;
|
||||
|
@ -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};
|
||||
|
@ -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)]
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -240,6 +240,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,
|
||||
|
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
[profile.release]
|
||||
overflow-checks = true
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "account-command"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "account_command"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
no-log-ix-name = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.25.0"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,55 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||
|
||||
#[program]
|
||||
pub mod account_command {
|
||||
use super::*;
|
||||
|
||||
pub fn initialize(
|
||||
ctx: Context<Initialize>,
|
||||
balance: f32,
|
||||
amount: u32,
|
||||
memo: String,
|
||||
values: Vec<u128>,
|
||||
) -> Result<()> {
|
||||
let my_account = &mut ctx.accounts.my_account;
|
||||
|
||||
my_account.balance = balance;
|
||||
my_account.delegate_pubkey = ctx.accounts.user.key().clone();
|
||||
my_account.sub = Sub {
|
||||
values,
|
||||
state: State::Confirmed { amount, memo },
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
|
||||
pub enum State {
|
||||
Pending,
|
||||
Confirmed { amount: u32, memo: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, AnchorDeserialize, AnchorSerialize)]
|
||||
pub struct Sub {
|
||||
pub values: Vec<u128>,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct MyAccount {
|
||||
pub balance: f32,
|
||||
pub delegate_pubkey: Pubkey,
|
||||
pub sub: Sub,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize<'info> {
|
||||
#[account(init, payer = user, space = 8 + 1000)]
|
||||
pub my_account: Account<'info, MyAccount>,
|
||||
#[account(mut)]
|
||||
pub user: Signer<'info>,
|
||||
pub system_program: Program<'info, System>,
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import * as anchor from "@project-serum/anchor";
|
||||
import { Program } from "@project-serum/anchor";
|
||||
import { AccountCommand } from "../target/types/account_command";
|
||||
import { assert } from "chai";
|
||||
import { execSync } from "child_process";
|
||||
import { sleep } from "@project-serum/common";
|
||||
|
||||
describe("Test CLI account commands", () => {
|
||||
// Configure the client to use the local cluster.
|
||||
const provider = anchor.AnchorProvider.env();
|
||||
|
||||
anchor.setProvider(provider);
|
||||
|
||||
const program = anchor.workspace.AccountCommand as Program<AccountCommand>;
|
||||
|
||||
it("Can fetch and deserialize account using the account command", async () => {
|
||||
const myAccount = anchor.web3.Keypair.generate();
|
||||
|
||||
const balance = -2.5;
|
||||
const amount = 108;
|
||||
const memo = "account test";
|
||||
const values = [1, 2, 3, 1000];
|
||||
|
||||
await program.methods
|
||||
.initialize(
|
||||
balance,
|
||||
new anchor.BN(amount),
|
||||
memo,
|
||||
values.map((x) => new anchor.BN(x))
|
||||
)
|
||||
.accounts({
|
||||
myAccount: myAccount.publicKey,
|
||||
user: provider.wallet.publicKey,
|
||||
systemProgram: anchor.web3.SystemProgram.programId,
|
||||
})
|
||||
.signers([myAccount])
|
||||
.rpc();
|
||||
|
||||
let output: any = {};
|
||||
for (let tries = 0; tries < 20; tries++) {
|
||||
try {
|
||||
output = JSON.parse(
|
||||
execSync(
|
||||
`anchor account account_command.MyAccount ${myAccount.publicKey}`,
|
||||
{ stdio: "pipe" }
|
||||
).toString()
|
||||
);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (!e.stderr.toString().startsWith("Error: AccountNotFound")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(5000);
|
||||
}
|
||||
|
||||
assert(output.balance == balance, "Balance deserialized incorrectly");
|
||||
assert(
|
||||
output.delegatePubkey == provider.wallet.publicKey,
|
||||
"delegatePubkey deserialized incorrectly"
|
||||
);
|
||||
assert(
|
||||
output.sub.state.Confirmed.amount === amount,
|
||||
"Amount deserialized incorrectly"
|
||||
);
|
||||
assert(
|
||||
output.sub.state.Confirmed.memo === memo,
|
||||
"Memo deserialized incorrectly"
|
||||
);
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
assert(
|
||||
output.sub.values[i] == values[i],
|
||||
"Values deserialized incorrectly"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
|
||||
},
|
||||
"workspaces": [
|
||||
"anchor-cli-account",
|
||||
"anchor-cli-idl",
|
||||
"cashiers-check",
|
||||
"cfo",
|
||||
|
|
Loading…
Reference in New Issue