diff --git a/api-server/api-server-common/src/lib.rs b/api-server/api-server-common/src/lib.rs index dce8861cdf..9d979da77a 100644 --- a/api-server/api-server-common/src/lib.rs +++ b/api-server/api-server-common/src/lib.rs @@ -16,6 +16,26 @@ pub mod storage; use clap::Parser; +use common::chain::config::ChainType; + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum Network { + Mainnet, + Testnet, + Regtest, + Signet, +} + +impl From for ChainType { + fn from(value: Network) -> Self { + match value { + Network::Mainnet => ChainType::Mainnet, + Network::Testnet => ChainType::Testnet, + Network::Regtest => ChainType::Regtest, + Network::Signet => ChainType::Signet, + } + } +} #[derive(Parser, Debug)] pub struct PostgresConfig { diff --git a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs index 1681f7ea9f..5ad0112690 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/mod.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/mod.rs @@ -15,14 +15,15 @@ pub mod transactional; -use std::collections::BTreeMap; - +use crate::storage::storage_api::{block_aux_data::BlockAuxData, ApiServerStorageError}; use common::{ chain::{Block, ChainConfig, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, +}; +use std::{ + collections::BTreeMap, + ops::Bound::{Excluded, Unbounded}, }; - -use crate::storage::storage_api::{block_aux_data::BlockAuxData, ApiServerStorageError}; use super::CURRENT_STORAGE_VERSION; @@ -30,6 +31,7 @@ use super::CURRENT_STORAGE_VERSION; struct ApiServerInMemoryStorage { block_table: BTreeMap, Block>, block_aux_data_table: BTreeMap, BlockAuxData>, + address_balance_table: BTreeMap>, main_chain_blocks_table: BTreeMap>, transaction_table: BTreeMap, (Option>, SignedTransaction)>, best_block: (BlockHeight, Id), @@ -41,6 +43,7 @@ impl ApiServerInMemoryStorage { let mut result = Self { block_table: BTreeMap::new(), block_aux_data_table: BTreeMap::new(), + address_balance_table: BTreeMap::new(), main_chain_blocks_table: BTreeMap::new(), transaction_table: BTreeMap::new(), best_block: (0.into(), chain_config.genesis_block_id()), @@ -56,6 +59,13 @@ impl ApiServerInMemoryStorage { Ok(true) } + fn get_address_balance(&self, address: &str) -> Result, ApiServerStorageError> { + self.address_balance_table.get(address).map_or_else( + || Ok(None), + |balance| Ok(balance.last_key_value().map(|(_, &v)| v)), + ) + } + fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { let block_result = self.block_table.get(&block_id); let block = match block_result { @@ -124,6 +134,40 @@ impl ApiServerInMemoryStorage { Ok(()) } + fn del_address_balance_above_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + // Inefficient, but acceptable for testing with InMemoryStorage + + self.address_balance_table.iter_mut().for_each(|(_, balance)| { + balance + .range((Excluded(block_height), Unbounded)) + .map(|b| b.0.into_int()) + .collect::>() + .iter() + .for_each(|&b| { + balance.remove(&BlockHeight::new(b)); + }) + }); + + Ok(()) + } + + fn set_address_balance_at_height( + &mut self, + address: &str, + amount: Amount, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + self.address_balance_table + .entry(address.to_string()) + .or_default() + .insert(block_height, amount); + + Ok(()) + } + fn set_block( &mut self, block_id: Id, diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs index ed1f474be1..bd454af4ad 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs @@ -15,7 +15,7 @@ use common::{ chain::{Block, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, }; use crate::storage::storage_api::{ @@ -30,6 +30,13 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'t> { self.transaction.is_initialized() } + async fn get_address_balance( + &self, + address: &str, + ) -> Result, ApiServerStorageError> { + self.transaction.get_address_balance(address) + } + async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError> { self.transaction.get_block(block_id) } diff --git a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs index a9d091e16c..a666d7a722 100644 --- a/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs @@ -15,7 +15,7 @@ use common::{ chain::{Block, ChainConfig, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, }; use crate::storage::storage_api::{ @@ -34,6 +34,22 @@ impl<'t> ApiServerStorageWrite for ApiServerInMemoryStorageTransactionalRw<'t> { self.transaction.initialize_storage(chain_config) } + async fn del_address_balance_above_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + self.transaction.del_address_balance_above_height(block_height) + } + + async fn set_address_balance_at_height( + &mut self, + address: &str, + amount: Amount, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + self.transaction.set_address_balance_at_height(address, amount, block_height) + } + async fn set_block( &mut self, block_id: Id, @@ -93,6 +109,13 @@ impl<'t> ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'t> { Ok(Some(self.transaction.get_storage_version()?)) } + async fn get_address_balance( + &self, + address: &str, + ) -> Result, ApiServerStorageError> { + self.transaction.get_address_balance(address) + } + async fn get_best_block(&self) -> Result<(BlockHeight, Id), ApiServerStorageError> { self.transaction.get_best_block() } diff --git a/api-server/api-server-common/src/storage/impls/postgres/queries.rs b/api-server/api-server-common/src/storage/impls/postgres/queries.rs index 7b36e9293c..f01b3c221e 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/queries.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/queries.rs @@ -18,7 +18,7 @@ use serialization::{DecodeAll, Encode}; use common::{ chain::{Block, ChainConfig, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, }; use tokio_postgres::NoTls; @@ -113,6 +113,80 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { Ok(Some(version)) } + pub async fn get_address_balance( + &self, + address: &str, + ) -> Result, ApiServerStorageError> { + self.tx + .query_opt( + r#" + SELECT amount + FROM ml_address_balance + WHERE address = $1 + ORDER BY block_height DESC + LIMIT 1; + "#, + &[&address], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))? + .map_or_else( + || Ok(None), + |row| { + let amount: Vec = row.get(0); + let amount = Amount::decode_all(&mut amount.as_slice()).map_err(|e| { + ApiServerStorageError::DeserializationError(format!( + "Amount deserialization failed: {}", + e + )) + })?; + + Ok(Some(amount)) + }, + ) + } + + pub async fn del_address_balance_above_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + let height = Self::block_height_to_postgres_friendly(block_height); + + self.tx + .execute( + "DELETE FROM ml_address_balance WHERE block_height > $1;", + &[&height], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; + + Ok(()) + } + + pub async fn set_address_balance_at_height( + &mut self, + address: &str, + amount: Amount, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + let height = Self::block_height_to_postgres_friendly(block_height); + + self.tx + .execute( + r#" + INSERT INTO ml_address_balance (address, block_height, amount) + VALUES ($1, $2, $3) + ON CONFLICT (address, block_height) + DO UPDATE SET amount = $3; + "#, + &[&address.to_string(), &height, &amount.encode()], + ) + .await + .map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?; + + Ok(()) + } + pub async fn get_best_block( &mut self, ) -> Result<(BlockHeight, Id), ApiServerStorageError> { @@ -203,6 +277,16 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> { ) .await?; + self.just_execute( + "CREATE TABLE ml_address_balance ( + address TEXT NOT NULL, + block_height bigint NOT NULL, + amount bytea NOT NULL, + PRIMARY KEY (address, block_height) + );", + ) + .await?; + self.just_execute( "CREATE TABLE ml_block_aux_data ( block_id bytea PRIMARY KEY, diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs index 29b631c86d..7ac259ad07 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs @@ -39,6 +39,19 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRo<'a> { Ok(res) } + async fn get_address_balance( + &self, + address: &str, + ) -> Result< + Option, + crate::storage::storage_api::ApiServerStorageError, + > { + let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.get_address_balance(address).await?; + + Ok(res) + } + async fn get_best_block( &self, ) -> Result< diff --git a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs index 6d0e33abb3..088f223452 100644 --- a/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs +++ b/api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs @@ -15,7 +15,7 @@ use common::{ chain::{Block, ChainConfig, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, }; use crate::storage::{ @@ -40,6 +40,28 @@ impl<'a> ApiServerStorageWrite for ApiServerPostgresTransactionalRw<'a> { Ok(()) } + async fn del_address_balance_above_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + conn.del_address_balance_above_height(block_height).await?; + + Ok(()) + } + + async fn set_address_balance_at_height( + &mut self, + address: &str, + amount: Amount, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError> { + let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + conn.set_address_balance_at_height(address, amount, block_height).await?; + + Ok(()) + } + async fn set_best_block( &mut self, block_height: BlockHeight, @@ -123,6 +145,16 @@ impl<'a> ApiServerStorageRead for ApiServerPostgresTransactionalRw<'a> { Ok(res) } + async fn get_address_balance( + &self, + address: &str, + ) -> Result, ApiServerStorageError> { + let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); + let res = conn.get_address_balance(address).await?; + + Ok(res) + } + async fn get_best_block(&self) -> Result<(BlockHeight, Id), ApiServerStorageError> { let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR)); let res = conn.get_best_block().await?; diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index 61a27ecafb..48fb3bccbd 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -15,10 +15,11 @@ use common::{ chain::{Block, ChainConfig, GenBlock, SignedTransaction, Transaction}, - primitives::{BlockHeight, Id}, + primitives::{Amount, BlockHeight, Id}, }; use self::block_aux_data::BlockAuxData; + pub mod block_aux_data; #[allow(dead_code)] @@ -50,6 +51,11 @@ pub trait ApiServerStorageRead: Sync { async fn get_storage_version(&self) -> Result, ApiServerStorageError>; + async fn get_address_balance( + &self, + address: &str, + ) -> Result, ApiServerStorageError>; + async fn get_best_block(&self) -> Result<(BlockHeight, Id), ApiServerStorageError>; async fn get_block(&self, block_id: Id) -> Result, ApiServerStorageError>; @@ -78,6 +84,18 @@ pub trait ApiServerStorageWrite: ApiServerStorageRead { chain_config: &ChainConfig, ) -> Result<(), ApiServerStorageError>; + async fn del_address_balance_above_height( + &mut self, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError>; + + async fn set_address_balance_at_height( + &mut self, + address: &str, + amount: Amount, + block_height: BlockHeight, + ) -> Result<(), ApiServerStorageError>; + async fn set_best_block( &mut self, block_height: BlockHeight, diff --git a/api-server/scanner-daemon/src/config.rs b/api-server/scanner-daemon/src/config.rs index 2aa2f9d3e2..458c1cdc18 100644 --- a/api-server/scanner-daemon/src/config.rs +++ b/api-server/scanner-daemon/src/config.rs @@ -13,19 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use api_server_common::PostgresConfig; +use api_server_common::{Network, PostgresConfig}; use clap::Parser; -use common::chain::config::ChainType; use std::net::SocketAddr; -#[derive(clap::ValueEnum, Clone, Debug)] -pub enum Network { - Mainnet, - Testnet, - Regtest, - Signet, -} - #[derive(Parser, Debug)] pub struct ApiServerScannerArgs { /// Network @@ -52,14 +43,3 @@ pub struct ApiServerScannerArgs { #[clap(flatten)] pub postgres_config: PostgresConfig, } - -impl From for ChainType { - fn from(value: Network) -> Self { - match value { - Network::Mainnet => ChainType::Mainnet, - Network::Testnet => ChainType::Testnet, - Network::Regtest => ChainType::Regtest, - Network::Signet => ChainType::Signet, - } - } -} diff --git a/api-server/scanner-daemon/src/main.rs b/api-server/scanner-daemon/src/main.rs index c51e018572..4f72e6c42d 100644 --- a/api-server/scanner-daemon/src/main.rs +++ b/api-server/scanner-daemon/src/main.rs @@ -82,7 +82,7 @@ pub async fn run( .unwrap_or_else(|e| panic!("Storage initialization commit failed {}", e)); } - let mut local_block = BlockchainState::new(storage); + let mut local_block = BlockchainState::new(Arc::clone(chain_config), storage); loop { let sync_result = api_blockchain_scanner_lib::sync::sync_once(chain_config, rpc_client, &mut local_block) diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 5ab0e36102..47f52881d3 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -19,9 +19,15 @@ use api_server_common::storage::storage_api::{ ApiServerTransactionRw, }; use common::{ - chain::{Block, GenBlock}, - primitives::{id::WithId, BlockHeight, Id, Idable}, + address::Address, + chain::{ + config::ChainConfig, output_value::OutputValue, transaction::OutPointSourceId, Block, + Destination, GenBlock, TxInput, TxOutput, + }, + primitives::{id::WithId, Amount, BlockHeight, Id, Idable}, }; +use std::ops::{Add, Sub}; +use std::sync::Arc; #[derive(Debug, thiserror::Error)] pub enum BlockchainStateError { @@ -30,12 +36,16 @@ pub enum BlockchainStateError { } pub struct BlockchainState { + chain_config: Arc, storage: S, } impl BlockchainState { - pub fn new(storage: S) -> Self { - Self { storage } + pub fn new(chain_config: Arc, storage: S) -> Self { + Self { + chain_config, + storage, + } } pub fn storage(&self) -> &S { @@ -48,8 +58,8 @@ impl LocalBlockchainState for BlockchainState type Error = BlockchainStateError; async fn best_block(&self) -> Result<(BlockHeight, Id), Self::Error> { - let db_tx = self.storage.transaction_ro().await?; - let best_block = db_tx.get_best_block().await?; + let db_tx = self.storage.transaction_ro().await.expect("Unable to connect to database"); + let best_block = db_tx.get_best_block().await.expect("Unable to get best block"); Ok(best_block) } @@ -58,36 +68,231 @@ impl LocalBlockchainState for BlockchainState common_block_height: BlockHeight, blocks: Vec, ) -> Result<(), Self::Error> { - let mut db_tx = self.storage.transaction_rw().await?; + let mut db_tx = self.storage.transaction_rw().await.expect("Unable to connect to database"); // Disconnect blocks from main-chain - while db_tx.get_best_block().await?.0 > common_block_height { + while db_tx.get_best_block().await.expect("Unable to get best block").0 + > common_block_height + { let current_best = db_tx.get_best_block().await?; logging::log::info!("Disconnecting block: {:?}", current_best); - db_tx.del_main_chain_block_id(current_best.0).await?; + + db_tx + .del_main_chain_block_id(current_best.0) + .await + .expect("Unable to disconnect block"); } + // Disconnect address balances + db_tx + .del_address_balance_above_height(common_block_height) + .await + .expect("Unable to disconnect address balance"); + // Connect the new blocks in the new chain for (index, block) in blocks.into_iter().map(WithId::new).enumerate() { let block_height = BlockHeight::new(common_block_height.into_int() + index as u64 + 1); - db_tx.set_main_chain_block_id(block_height, block.get_id()).await?; + db_tx + .set_main_chain_block_id(block_height, block.get_id()) + .await + .expect("Unable to connect block"); + logging::log::info!("Connected block: ({}, {})", block_height, block.get_id()); + update_balances_from_outputs( + Arc::clone(&self.chain_config), + &mut db_tx, + block_height, + block.block_reward().outputs(), + ) + .await + .expect("Unable to update balances from block reward outputs"); + for tx in block.transactions() { db_tx .set_transaction(tx.transaction().get_id(), Some(block.get_id()), tx) - .await?; + .await + .expect("Unable to set transaction"); + + update_balances_from_inputs( + Arc::clone(&self.chain_config), + &mut db_tx, + block_height, + tx.inputs(), + ) + .await + .expect("Unable to update balances from inputs"); + + update_balances_from_outputs( + Arc::clone(&self.chain_config), + &mut db_tx, + block_height, + tx.outputs(), + ) + .await + .expect("Unable to update balances from transaction outputs"); } - db_tx.set_block(block.get_id(), &block).await?; - db_tx.set_best_block(block_height, block.get_id().into()).await?; - } + db_tx.set_block(block.get_id(), &block).await.expect("Unable to set block"); - db_tx.commit().await?; + db_tx + .set_best_block(block_height, block.get_id().into()) + .await + .expect("Unable to set best block"); + } + db_tx.commit().await.expect("Unable to commit transaction"); logging::log::info!("Database commit completed successfully"); Ok(()) } } + +async fn update_balances_from_inputs( + chain_config: Arc, + db_tx: &mut T, + block_height: BlockHeight, + inputs: &[TxInput], +) -> Result<(), ApiServerStorageError> { + for input in inputs { + match input { + TxInput::Account(_) => { + // TODO + } + TxInput::Utxo(outpoint) => { + match outpoint.source_id() { + OutPointSourceId::BlockReward(_block_id) => { + // TODO + } + OutPointSourceId::Transaction(transaction_id) => { + let input_transaction = db_tx + .get_transaction(transaction_id) + .await? + .expect("Transaction should exist"); + + assert!( + input_transaction.1.transaction().outputs().len() + > outpoint.output_index() as usize + ); + + match &input_transaction.1.transaction().outputs() + [outpoint.output_index() as usize] + { + TxOutput::Burn(_) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::CreateStakePool(_, _) + | TxOutput::DataDeposit(_) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::ProduceBlockFromStake(_, _) => {} + TxOutput::LockThenTransfer(output_value, destination, _) + | TxOutput::Transfer(output_value, destination) => { + match destination { + Destination::PublicKey(_) | Destination::Address(_) => { + let address = + Address::::new(&chain_config, destination) + .expect("Unable to encode destination"); + + match output_value { + OutputValue::TokenV0(_) + | OutputValue::TokenV1(_, _) => { + // TODO + } + OutputValue::Coin(amount) => { + let current_balance = db_tx + .get_address_balance(address.get()) + .await + .expect("Unable to get balance") + .unwrap_or(Amount::ZERO); + + let new_amount = current_balance + .sub(*amount) + .expect("Balance should not underflow"); + + db_tx + .set_address_balance_at_height( + address.get(), + new_amount, + block_height, + ) + .await + .expect("Unable to update balance") + } + } + } + Destination::AnyoneCanSpend + | Destination::ClassicMultisig(_) + | Destination::ScriptHash(_) => {} + } + } + } + } + } + } + } + } + + Ok(()) +} + +async fn update_balances_from_outputs( + chain_config: Arc, + db_tx: &mut T, + block_height: BlockHeight, + outputs: &[TxOutput], +) -> Result<(), ApiServerStorageError> { + for output in outputs { + match output { + TxOutput::Burn(_) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::CreateStakePool(_, _) + | TxOutput::DataDeposit(_) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::ProduceBlockFromStake(_, _) => {} + TxOutput::Transfer(output_value, destination) + | TxOutput::LockThenTransfer(output_value, destination, _) => { + match destination { + Destination::PublicKey(_) | Destination::Address(_) => { + let address = Address::::new(&chain_config, destination) + .expect("Unable to encode destination"); + + match output_value { + OutputValue::TokenV0(_) | OutputValue::TokenV1(_, _) => { + // TODO + } + OutputValue::Coin(amount) => { + let current_balance = db_tx + .get_address_balance(address.get()) + .await + .expect("Unable to get balance") + .unwrap_or(Amount::ZERO); + + let new_amount = current_balance + .add(*amount) + .expect("Balance should not overflow"); + + db_tx + .set_address_balance_at_height( + address.get(), + new_amount, + block_height, + ) + .await + .expect("Unable to update balance") + } + } + } + Destination::AnyoneCanSpend + | Destination::ClassicMultisig(_) + | Destination::ScriptHash(_) => {} + } + } + } + } + + Ok(()) +} diff --git a/api-server/stack-test-suite/tests/v1/address.rs b/api-server/stack-test-suite/tests/v1/address.rs new file mode 100644 index 0000000000..7c5506c8b4 --- /dev/null +++ b/api-server/stack-test-suite/tests/v1/address.rs @@ -0,0 +1,474 @@ +// Copyright (c) 2023 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +#[tokio::test] +async fn invalid_address() { + let (task, response) = spawn_webserver("/api/v1/address/invalid-address").await; + + assert_eq!(response.status(), 400); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body["error"].as_str().unwrap(), "Invalid address"); + + task.abort(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test] +async fn address_not_found(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = create_unit_test_config(); + + let (_, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let destination = Destination::Address(PublicKeyHash::from(&public_key)); + let address = Address::::new(&chain_config, &destination).unwrap(); + + let (task, response) = spawn_webserver(&format!("/api/v1/address/{}", address.get())).await; + + assert_eq!(response.status(), 400); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body["error"].as_str().unwrap(), "Address not found"); + + task.abort(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test] +async fn multiple_outputs_to_single_address(#[case] seed: Seed) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let task = tokio::spawn(async move { + let web_server_state = { + let mut rng = make_seedable_rng(seed); + let chain_config = create_unit_test_config(); + + let chainstate_blocks = { + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config(chain_config.clone()) + .build(); + + // generate addresses + + let (alice_sk, alice_pk) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let alice_destination = Destination::Address(PublicKeyHash::from(&alice_pk)); + let alice_address = + Address::::new(&chain_config, &alice_destination).unwrap(); + let mut alice_balance = Amount::from_atoms(1_000_000); + + let (_bob_sk, bob_pk) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let bob_destination = Destination::Address(PublicKeyHash::from(&bob_pk)); + let bob_address = + Address::::new(&chain_config, &bob_destination).unwrap(); + let mut bob_balance = Amount::ZERO; + + // setup initial transaction + + let previous_tx_out = + TxOutput::Transfer(OutputValue::Coin(alice_balance), alice_destination.clone()); + + let transaction = TransactionBuilder::new() + .add_input( + TxInput::from_utxo( + OutPointSourceId::BlockReward(tf.genesis().get_id().into()), + 0, + ), + InputWitness::NoSignature(None), + ) + .add_output(previous_tx_out.clone()) + .build(); + + let previous_transaction_id = transaction.transaction().get_id(); + + let mut previous_witness = InputWitness::Standard( + StandardInputSignature::produce_uniparty_signature_for_input( + &alice_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + alice_destination.clone(), + &transaction, + &[Some(&previous_tx_out)], + 0, + ) + .unwrap(), + ); + + let mut chainstate_block_ids = vec![*tf + .make_block_builder() + .add_transaction(transaction.clone()) + .build_and_process() + .unwrap() + .unwrap() + .block_id()]; + + // Generate two outputs for a single transaction + + let random_coin_amount1 = rng.gen_range(1..10); + let random_coin_amount2 = rng.gen_range(1..10); + + alice_balance = (alice_balance - Amount::from_atoms(random_coin_amount1)).unwrap(); + alice_balance = (alice_balance - Amount::from_atoms(random_coin_amount2)).unwrap(); + + bob_balance = (bob_balance + Amount::from_atoms(random_coin_amount1)).unwrap(); + bob_balance = (bob_balance + Amount::from_atoms(random_coin_amount2)).unwrap(); + + let alice_tx_out = + TxOutput::Transfer(OutputValue::Coin(alice_balance), alice_destination.clone()); + + let bob_tx_out1 = TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(random_coin_amount1)), + bob_destination.clone(), + ); + + let bob_tx_out2 = TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(random_coin_amount2)), + bob_destination.clone(), + ); + + let transaction = TransactionBuilder::new() + .add_input( + TxInput::from_utxo( + OutPointSourceId::Transaction(previous_transaction_id), + 0, + ), + previous_witness.clone(), + ) + .add_output(alice_tx_out.clone()) + .add_output(bob_tx_out1.clone()) + .add_output(bob_tx_out2.clone()) + .build(); + + previous_witness = InputWitness::Standard( + StandardInputSignature::produce_uniparty_signature_for_input( + &alice_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + alice_destination.clone(), + &transaction, + &[Some(&previous_tx_out)], + 0, + ) + .unwrap(), + ); + + let signed_transaction = SignedTransaction::new( + transaction.transaction().clone(), + vec![previous_witness.clone()], + ) + .unwrap(); + + chainstate_block_ids.push( + *tf.make_block_builder() + .add_transaction(signed_transaction) + .build_and_process() + .unwrap() + .unwrap() + .block_id(), + ); + + _ = tx.send([ + ( + alice_address.get().to_string(), + json!({ + "coin_balance": alice_balance.into_atoms(), + }), + ), + ( + bob_address.to_string(), + json!({ + "coin_balance": bob_balance.into_atoms(), + }), + ), + ]); + + chainstate_block_ids + .iter() + .map(|id| tf.block(tf.to_chain_block_id(id.into()))) + .collect::>() + }; + + let storage = { + let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config); + + let mut db_tx = storage.transaction_rw().await.unwrap(); + db_tx.initialize_storage(&chain_config).await.unwrap(); + db_tx.commit().await.unwrap(); + + storage + }; + + let chain_config = Arc::new(chain_config); + + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); + local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); + + ApiServerWebServerState { + db: Arc::new(local_node.storage().clone_storage().await), + chain_config: Arc::clone(&chain_config), + } + }; + + web_server(listener, web_server_state).await + }); + + for (address, expected_balance) in rx.await.unwrap() { + let url = format!("/api/v1/address/{address}"); + + // Given that the listener port is open, this will block until a + // response is made (by the web server, which takes the listener + // over) + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + + assert_eq!( + response.status(), + 200, + "Failed getting address balance for {address}" + ); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body, expected_balance); + } + + task.abort(); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +#[tokio::test] +async fn ok(#[case] seed: Seed) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let task = tokio::spawn(async move { + let web_server_state = { + let mut rng = make_seedable_rng(seed); + let chain_config = create_unit_test_config(); + + let chainstate_blocks = { + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config(chain_config.clone()) + .build(); + + // generate addresses + + let (alice_sk, alice_pk) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let alice_destination = Destination::Address(PublicKeyHash::from(&alice_pk)); + let alice_address = + Address::::new(&chain_config, &alice_destination).unwrap(); + let mut alice_balance = Amount::from_atoms(1_000_000); + + let (_bob_sk, bob_pk) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let bob_destination = Destination::Address(PublicKeyHash::from(&bob_pk)); + let bob_address = + Address::::new(&chain_config, &bob_destination).unwrap(); + let mut bob_balance = Amount::ZERO; + + // setup initial transaction + + let mut previous_tx_out = + TxOutput::Transfer(OutputValue::Coin(alice_balance), alice_destination.clone()); + + let transaction = TransactionBuilder::new() + .add_input( + TxInput::from_utxo( + OutPointSourceId::BlockReward(tf.genesis().get_id().into()), + 0, + ), + InputWitness::NoSignature(None), + ) + .add_output(previous_tx_out.clone()) + .build(); + + let mut previous_transaction_id = transaction.transaction().get_id(); + + let mut previous_witness = InputWitness::Standard( + StandardInputSignature::produce_uniparty_signature_for_input( + &alice_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + alice_destination.clone(), + &transaction, + &[Some(&previous_tx_out)], + 0, + ) + .unwrap(), + ); + + let mut chainstate_block_ids = vec![*tf + .make_block_builder() + .add_transaction(transaction.clone()) + .build_and_process() + .unwrap() + .unwrap() + .block_id()]; + + for _ in 0..rng.gen_range(1..100) { + let random_coin_amount = rng.gen_range(1..10); + + alice_balance = + (alice_balance - Amount::from_atoms(random_coin_amount)).unwrap(); + + bob_balance = (bob_balance + Amount::from_atoms(random_coin_amount)).unwrap(); + + let alice_tx_out = TxOutput::Transfer( + OutputValue::Coin(alice_balance), + alice_destination.clone(), + ); + + let bob_tx_out = TxOutput::Transfer( + OutputValue::Coin(Amount::from_atoms(random_coin_amount)), + bob_destination.clone(), + ); + + let transaction = TransactionBuilder::new() + .add_input( + TxInput::from_utxo( + OutPointSourceId::Transaction(previous_transaction_id), + 0, + ), + previous_witness.clone(), + ) + .add_output(alice_tx_out.clone()) + .add_output(bob_tx_out.clone()) + .build(); + + previous_transaction_id = transaction.transaction().get_id(); + + previous_witness = InputWitness::Standard( + StandardInputSignature::produce_uniparty_signature_for_input( + &alice_sk, + SigHashType::try_from(SigHashType::ALL).unwrap(), + alice_destination.clone(), + &transaction, + &[Some(&previous_tx_out)], + 0, + ) + .unwrap(), + ); + + let signed_transaction = SignedTransaction::new( + transaction.transaction().clone(), + vec![previous_witness.clone()], + ) + .unwrap(); + + chainstate_block_ids.push( + *tf.make_block_builder() + .add_transaction(signed_transaction) + .build_and_process() + .unwrap() + .unwrap() + .block_id(), + ); + + previous_tx_out = alice_tx_out; + } + + _ = tx.send([ + ( + alice_address.get().to_string(), + json!({ + "coin_balance": alice_balance.into_atoms(), + }), + ), + ( + bob_address.to_string(), + json!({ + "coin_balance": bob_balance.into_atoms(), + }), + ), + ]); + + chainstate_block_ids + .iter() + .map(|id| tf.block(tf.to_chain_block_id(id.into()))) + .collect::>() + }; + + let storage = { + let mut storage = TransactionalApiServerInMemoryStorage::new(&chain_config); + + let mut db_tx = storage.transaction_rw().await.unwrap(); + db_tx.initialize_storage(&chain_config).await.unwrap(); + db_tx.commit().await.unwrap(); + + storage + }; + + let chain_config = Arc::new(chain_config); + + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); + local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); + + ApiServerWebServerState { + db: Arc::new(local_node.storage().clone_storage().await), + chain_config: Arc::clone(&chain_config), + } + }; + + web_server(listener, web_server_state).await + }); + + for (address, expected_balance) in rx.await.unwrap() { + let url = format!("/api/v1/address/{address}"); + + // Given that the listener port is open, this will block until a + // response is made (by the web server, which takes the listener + // over) + let response = reqwest::get(format!("http://{}:{}{url}", addr.ip(), addr.port())) + .await + .unwrap(); + + assert_eq!( + response.status(), + 200, + "Failed getting address balance for {address}" + ); + + let body = response.text().await.unwrap(); + let body: serde_json::Value = serde_json::from_str(&body).unwrap(); + + assert_eq!(body, expected_balance); + } + + task.abort(); +} + +// TODO test address balances after a reorg diff --git a/api-server/stack-test-suite/tests/v1/block.rs b/api-server/stack-test-suite/tests/v1/block.rs index 4919126fa2..73d527deb4 100644 --- a/api-server/stack-test-suite/tests/v1/block.rs +++ b/api-server/stack-test-suite/tests/v1/block.rs @@ -108,12 +108,13 @@ async fn ok(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/block_header.rs b/api-server/stack-test-suite/tests/v1/block_header.rs index 4fd5bb732a..5199ac6251 100644 --- a/api-server/stack-test-suite/tests/v1/block_header.rs +++ b/api-server/stack-test-suite/tests/v1/block_header.rs @@ -102,12 +102,13 @@ async fn ok(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/block_reward.rs b/api-server/stack-test-suite/tests/v1/block_reward.rs index a0727198c9..b69c5c5b4f 100644 --- a/api-server/stack-test-suite/tests/v1/block_reward.rs +++ b/api-server/stack-test-suite/tests/v1/block_reward.rs @@ -95,12 +95,13 @@ async fn no_reward(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; @@ -184,12 +185,13 @@ async fn has_reward(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), vec![block]).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/block_transaction_ids.rs b/api-server/stack-test-suite/tests/v1/block_transaction_ids.rs index 9a41a4ac86..7343e6dd95 100644 --- a/api-server/stack-test-suite/tests/v1/block_transaction_ids.rs +++ b/api-server/stack-test-suite/tests/v1/block_transaction_ids.rs @@ -103,12 +103,13 @@ async fn ok(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/chain_at_height.rs b/api-server/stack-test-suite/tests/v1/chain_at_height.rs index e90c7aeb90..43bc9b1899 100644 --- a/api-server/stack-test-suite/tests/v1/chain_at_height.rs +++ b/api-server/stack-test-suite/tests/v1/chain_at_height.rs @@ -113,12 +113,13 @@ async fn height_n(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/chain_tip.rs b/api-server/stack-test-suite/tests/v1/chain_tip.rs index 5f720bb257..5137725262 100644 --- a/api-server/stack-test-suite/tests/v1/chain_tip.rs +++ b/api-server/stack-test-suite/tests/v1/chain_tip.rs @@ -120,12 +120,13 @@ async fn height_n(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/mod.rs b/api-server/stack-test-suite/tests/v1/mod.rs index d7d1a9426e..9027cdb42d 100644 --- a/api-server/stack-test-suite/tests/v1/mod.rs +++ b/api-server/stack-test-suite/tests/v1/mod.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod address; mod block; mod block_header; mod block_reward; @@ -32,14 +33,22 @@ use api_server_common::storage::{ }; use api_web_server::{api::web_server, ApiServerWebServerState}; use chainstate::BlockSource; -use chainstate_test_framework::TestFramework; +use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::{ + address::{pubkeyhash::PublicKeyHash, Address}, chain::{ - config::create_unit_test_config, output_value::OutputValue, - transaction::output::timelock::OutputTimeLock, Destination, TxOutput, + config::create_unit_test_config, + output_value::OutputValue, + signature::{ + inputsig::{standard_signature::StandardInputSignature, InputWitness}, + sighash::sighashtype::SigHashType, + }, + transaction::output::timelock::OutputTimeLock, + Destination, OutPointSourceId, SignedTransaction, TxInput, TxOutput, }, primitives::{Amount, BlockHeight, Idable}, }; +use crypto::key::{KeyKind, PrivateKey}; use hex::ToHex; use rstest::rstest; use serde_json::json; diff --git a/api-server/stack-test-suite/tests/v1/transaction.rs b/api-server/stack-test-suite/tests/v1/transaction.rs index e76c620b52..1ae21d6714 100644 --- a/api-server/stack-test-suite/tests/v1/transaction.rs +++ b/api-server/stack-test-suite/tests/v1/transaction.rs @@ -112,12 +112,13 @@ async fn ok(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs b/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs index 0172a007b7..71d8c4fa45 100644 --- a/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs +++ b/api-server/stack-test-suite/tests/v1/transaction_merkle_path.rs @@ -93,7 +93,8 @@ async fn get_block_failed(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); storage = { @@ -118,7 +119,7 @@ async fn get_block_failed(#[case] seed: Seed) { ApiServerWebServerState { db: Arc::new(storage), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; @@ -203,7 +204,8 @@ async fn transaction_not_part_of_block(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); storage = { @@ -218,7 +220,7 @@ async fn transaction_not_part_of_block(#[case] seed: Seed) { ApiServerWebServerState { db: Arc::new(storage), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; @@ -306,7 +308,8 @@ async fn cannot_find_transaction_in_block(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); storage = { @@ -336,7 +339,7 @@ async fn cannot_find_transaction_in_block(#[case] seed: Seed) { ApiServerWebServerState { db: Arc::new(storage), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; @@ -438,12 +441,13 @@ async fn ok(#[case] seed: Seed) { storage }; - let mut local_node = BlockchainState::new(storage); + let chain_config = Arc::new(chain_config); + let mut local_node = BlockchainState::new(Arc::clone(&chain_config), storage); local_node.scan_blocks(BlockHeight::new(0), chainstate_blocks).await.unwrap(); ApiServerWebServerState { db: Arc::new(local_node.storage().clone_storage().await), - chain_config: Arc::new(chain_config), + chain_config: Arc::clone(&chain_config), } }; diff --git a/api-server/web-server/src/api/v1.rs b/api-server/web-server/src/api/v1.rs index 6808ad910b..8b82567caf 100644 --- a/api-server/web-server/src/api/v1.rs +++ b/api-server/web-server/src/api/v1.rs @@ -24,7 +24,8 @@ use axum::{ Json, Router, }; use common::{ - chain::{Block, SignedTransaction, Transaction}, + address::Address, + chain::{Block, Destination, SignedTransaction, Transaction}, primitives::{BlockHeight, Id, Idable, H256}, }; use crypto::random::{make_true_rng, Rng}; @@ -55,23 +56,7 @@ pub fn routes( .route("/transaction/:id", get(transaction)) .route("/transaction/:id/merkle-path", get(transaction_merkle_path)); - let router = router - .route( - "/destination/address/:public_key_hash", - get(destination_address), - ) - .route( - "/destination/public-key/:public_key", - get(destination_public_key), - ) - .route( - "/destination/script-hash/:script_id", - get(destination_script_hash), - ) - .route( - "/destination/multisig/:public_key", - get(destination_multisig), - ); + let router = router.route("/address/:address", get(address)); router.route("/pool/:id", get(pool)) } @@ -355,99 +340,54 @@ pub async fn transaction_merkle_path( } // -// destination/ +// address/ // #[allow(clippy::unused_async)] -pub async fn destination_address( - Path(_public_key_hash): Path, -) -> Result { - // TODO replace mock with database calls - - let mut rng = make_true_rng(); - - Ok(Json(json!({ - "balance": rng.gen_range(1..100_000_000), - "tokens": { - "BTC": rng.gen_range(1..1000), - "ETH": rng.gen_range(1..1000), - "USDT": rng.gen_range(1..1000), - "USDC": rng.gen_range(1..1000), - }, - "history": (0..rng.gen_range(1..20)).map(|_| { json!({ - "block_id": Id::::new(H256::random_using(&mut rng)), - "transaction_id": Id::::new(H256::random_using(&mut rng)), - })}).collect::>(), - }))) -} - -#[allow(clippy::unused_async)] -pub async fn destination_multisig( - Path(_public_key): Path, -) -> Result { - // TODO replace mock with database calls - - let mut rng = make_true_rng(); - - Ok(Json(json!({ - "balance": rng.gen_range(1..100_000_000), - "tokens": { - "BTC": rng.gen_range(1..1000), - "ETH": rng.gen_range(1..1000), - "USDT": rng.gen_range(1..1000), - "USDC": rng.gen_range(1..1000), - }, - "history": (0..rng.gen_range(1..20)).map(|_| { json!({ - "block_id": Id::::new(H256::random_using(&mut rng)), - "transaction_id": Id::::new(H256::random_using(&mut rng)), - })}).collect::>(), - }))) -} - -#[allow(clippy::unused_async)] -pub async fn destination_public_key( - Path(_public_key): Path, +pub async fn address( + Path(address): Path, + State(state): State>>, ) -> Result { - // TODO replace mock with database calls + let address = + Address::::from_str(&state.chain_config, &address).map_err(|_| { + ApiServerWebServerError::ClientError(ApiServerWebServerClientError::InvalidAddress) + })?; - let mut rng = make_true_rng(); + let coin_balance = state + .db + .transaction_ro() + .await + .map_err(|_| { + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })? + .get_address_balance(&address.to_string()) + .await + .map_err(|_| { + ApiServerWebServerError::ServerError(ApiServerWebServerServerError::InternalServerError) + })? + .ok_or(ApiServerWebServerError::ClientError( + ApiServerWebServerClientError::AddressNotFound, + ))?; Ok(Json(json!({ - "balance": rng.gen_range(1..100_000_000), - "tokens": { - "BTC": rng.gen_range(1..1000), - "ETH": rng.gen_range(1..1000), - "USDT": rng.gen_range(1..1000), - "USDC": rng.gen_range(1..1000), - }, - "history": (0..rng.gen_range(1..20)).map(|_| { json!({ - "block_id": Id::::new(H256::random_using(&mut rng)), - "transaction_id": Id::::new(H256::random_using(&mut rng)), - })}).collect::>(), + "coin_balance": coin_balance.into_atoms(), + //TODO "token_balances": destination_summary.token_balances(), + //TODO "transaction_history": destination_summary.transaction_history(), }))) -} - -#[allow(clippy::unused_async)] -pub async fn destination_script_hash( - Path(_script_hash): Path, -) -> Result { - // TODO replace mock with database calls - - let mut rng = make_true_rng(); - Ok(Json(json!({ - "balance": rng.gen_range(1..100_000_000), - "tokens": { - "BTC": rng.gen_range(1..1000), - "ETH": rng.gen_range(1..1000), - "USDT": rng.gen_range(1..1000), - "USDC": rng.gen_range(1..1000), - }, - "history": (0..rng.gen_range(1..20)).map(|_| { json!({ - "block_id": Id::::new(H256::random_using(&mut rng)), - "transaction_id": Id::::new(H256::random_using(&mut rng)), - })}).collect::>(), - }))) + // Ok(Json(json!({ + // "balance": rng.gen_range(1..100_000_000), + // "tokens": { + // "BTC": rng.gen_range(1..1000), + // "ETH": rng.gen_range(1..1000), + // "USDT": rng.gen_range(1..1000), + // "USDC": rng.gen_range(1..1000), + // }, + // "history": (0..rng.gen_range(1..20)).map(|_| { json!({ + // "block_id": Id::::new(H256::random_using(&mut rng)), + // "transaction_id": Id::::new(H256::random_using(&mut rng)), + // })}).collect::>(), + // }))) } // diff --git a/api-server/web-server/src/config.rs b/api-server/web-server/src/config.rs index 262b1d2b51..dd5cf32d52 100644 --- a/api-server/web-server/src/config.rs +++ b/api-server/web-server/src/config.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use api_server_common::PostgresConfig; +use api_server_common::{Network, PostgresConfig}; use clap::Parser; use std::{ net::{SocketAddr, TcpListener}, @@ -24,6 +24,12 @@ const LISTEN_ADDRESS: &str = "127.0.0.1:3000"; #[derive(Debug, Parser)] pub struct ApiServerWebServerConfig { + /// Network + /// Default: `testnet` + /// Options: `mainnet`, `testnet`, `regtest`, `signet` + #[clap(long, value_enum, default_value_t = Network::Testnet)] + pub network: Network, + /// The optional network address and port to listen on /// /// Format: `:` diff --git a/api-server/web-server/src/error.rs b/api-server/web-server/src/error.rs index 9b91bf75da..96407ae30c 100644 --- a/api-server/web-server/src/error.rs +++ b/api-server/web-server/src/error.rs @@ -33,6 +33,8 @@ pub enum ApiServerWebServerError { #[allow(dead_code)] #[derive(Debug, Error, Serialize)] pub enum ApiServerWebServerClientError { + #[error("Address not found")] + AddressNotFound, #[error("Bad request")] BadRequest, #[error("Block not found")] @@ -41,6 +43,8 @@ pub enum ApiServerWebServerClientError { InvalidBlockHeight, #[error("Invalid block Id")] InvalidBlockId, + #[error("Invalid address")] + InvalidAddress, #[error("Invalid transaction Id")] InvalidTransactionId, #[error("No block found at supplied height")] diff --git a/api-server/web-server/src/main.rs b/api-server/web-server/src/main.rs index 3468ea077e..2f5b82b09a 100644 --- a/api-server/web-server/src/main.rs +++ b/api-server/web-server/src/main.rs @@ -20,7 +20,7 @@ mod error; use api_server_common::storage::impls::postgres::TransactionalApiServerPostgresStorage; use api_web_server::{api::web_server, config::ApiServerWebServerConfig, ApiServerWebServerState}; use clap::Parser; -use common::chain::config::create_unit_test_config; +use common::chain::config::Builder; use logging::log; use std::sync::Arc; @@ -35,8 +35,7 @@ async fn main() { let args = ApiServerWebServerConfig::parse(); log::info!("Command line options: {args:?}"); - // TODO: generalize network configuration - let chain_config = Arc::new(create_unit_test_config()); + let chain_config = Arc::new(Builder::new(args.network.into()).build()); let storage = TransactionalApiServerPostgresStorage::new( &args.postgres_config.postgres_host,