diff --git a/book/src/ref/commands.md b/book/src/ref/commands.md index 7d0e97b..7f0f112 100644 --- a/book/src/ref/commands.md +++ b/book/src/ref/commands.md @@ -16,6 +16,7 @@ Starkli offers the following commands: - state-update - transaction-receipt - chain-id +- balance - nonce - storage - class-hash-at diff --git a/src/main.rs b/src/main.rs index 33bc843..dfc886b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,6 +64,8 @@ enum Subcommands { TransactionReceipt(TransactionReceipt), #[clap(about = "Get Starknet network ID")] ChainId(ChainId), + #[clap(about = "Get native gas token (currently ETH) balance")] + Balance(Balance), #[clap(about = "Get nonce for a certain contract")] Nonce(Nonce), #[clap(about = "Get storage value for a slot at a contract")] @@ -135,6 +137,7 @@ async fn run_command(cli: Cli) -> Result<()> { Subcommands::StateUpdate(cmd) => cmd.run().await, Subcommands::TransactionReceipt(cmd) => cmd.run().await, Subcommands::ChainId(cmd) => cmd.run().await, + Subcommands::Balance(cmd) => cmd.run().await, Subcommands::Nonce(cmd) => cmd.run().await, Subcommands::Storage(cmd) => cmd.run().await, Subcommands::ClassHashAt(cmd) => cmd.run().await, diff --git a/src/subcommands/balance.rs b/src/subcommands/balance.rs new file mode 100644 index 0000000..3f5cfc0 --- /dev/null +++ b/src/subcommands/balance.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use anyhow::Result; +use bigdecimal::BigDecimal; +use clap::Parser; +use num_bigint::{BigUint, ToBigInt}; +use starknet::{ + core::types::{BlockId, BlockTag, FieldElement, FunctionCall}, + macros::selector, + providers::Provider, +}; + +use crate::{ + address_book::AddressBookResolver, decode::FeltDecoder, verbosity::VerbosityArgs, ProviderArgs, +}; + +/// The default ETH address: 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7. +const DEFAULT_ETH_ADDRESS: FieldElement = FieldElement::from_mont([ + 4380532846569209554, + 17839402928228694863, + 17240401758547432026, + 418961398025637529, +]); + +#[derive(Debug, Parser)] +pub struct Balance { + #[clap(flatten)] + provider: ProviderArgs, + #[clap(help = "Account address")] + account_address: String, + #[clap( + long, + conflicts_with = "hex", + help = "Display raw balance amount in integer" + )] + raw: bool, + #[clap( + long, + conflicts_with = "raw", + help = "Display balance amount in hexadecimal representation" + )] + hex: bool, + #[clap(flatten)] + verbosity: VerbosityArgs, +} + +impl Balance { + pub async fn run(self) -> Result<()> { + self.verbosity.setup_logging(); + + let provider = Arc::new(self.provider.into_provider()); + let felt_decoder = FeltDecoder::new(AddressBookResolver::new(provider.clone())); + + let account_address = felt_decoder + .decode_single_with_addr_fallback(&self.account_address) + .await?; + + let result = provider + .call( + FunctionCall { + contract_address: DEFAULT_ETH_ADDRESS, + entry_point_selector: selector!("balanceOf"), + calldata: vec![account_address], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?; + + if result.len() != 2 { + anyhow::bail!("unexpected call result size: {}", result.len()); + } + + let low = BigUint::from_bytes_be(&result[0].to_bytes_be()); + let high = BigUint::from_bytes_be(&result[1].to_bytes_be()); + + let raw_balance: BigUint = (high << 128) + low; + + if self.raw { + println!("{}", raw_balance); + } else if self.hex { + println!("{:#x}", raw_balance); + } else { + // `to_bigint()` from `BigUint` always returns `Some`. + let balance_dec = BigDecimal::new(raw_balance.to_bigint().unwrap(), 18); + println!("{}", balance_dec); + } + + Ok(()) + } +} diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index 6f3bf95..22ab466 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -49,6 +49,9 @@ pub use class_at::ClassAt; mod class_hash_at; pub use class_hash_at::ClassHashAt; +mod balance; +pub use balance::Balance; + mod nonce; pub use nonce::Nonce;