diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index 3dc57f7c72..90b13b6af7 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -1,17 +1,14 @@ pub mod service; -use kaspa_addresses::Version; -use kaspa_consensus_core::tx::{SignableTransaction, Transaction, UtxoEntry}; +use kaspa_consensus_core::tx::Transaction; use kaspa_wallet_core::api::WalletApi; -use kaspa_wallet_core::tx::{Signer, SignerT}; use kaspa_wallet_core::{ - api::{AccountsGetUtxosRequest, AccountsSendRequest, NewAddressKind}, + api::{AccountsGetUtxosRequest, NewAddressKind}, prelude::Address, - tx::{Fees, PaymentDestination, PaymentOutputs}, }; use kaspa_wallet_grpc_core::convert::{deserialize_txs, extract_tx}; use kaspa_wallet_grpc_core::kaspawalletd::{ - fee_policy::FeePolicy, kaspawalletd_server::Kaspawalletd, BroadcastRequest, BroadcastResponse, BumpFeeRequest, BumpFeeResponse, + kaspawalletd_server::Kaspawalletd, BroadcastRequest, BroadcastResponse, BumpFeeRequest, BumpFeeResponse, CreateUnsignedTransactionsRequest, CreateUnsignedTransactionsResponse, GetBalanceRequest, GetBalanceResponse, GetExternalSpendableUtxOsRequest, GetExternalSpendableUtxOsResponse, GetVersionRequest, GetVersionResponse, NewAddressRequest, NewAddressResponse, SendRequest, SendResponse, ShowAddressesRequest, ShowAddressesResponse, ShutdownRequest, ShutdownResponse, @@ -51,17 +48,10 @@ impl Kaspawalletd for Service { ) -> Result, Status> { let CreateUnsignedTransactionsRequest { address, amount, from, use_existing_change_address, is_send_all, fee_policy } = request.into_inner(); - let to_address = Address::try_from(address).map_err(|err| Status::invalid_argument(err.to_string()))?; - let (fee_rate, max_fee) = self.calculate_fee_limits(fee_policy).await?; - let from_addresses = from - .iter() - .map(|a| Address::try_from(a.as_str())) - .collect::, _>>() - .map_err(|err| Status::invalid_argument(err.to_string()))?; - let transactions = - self.unsigned_txs(to_address, amount, use_existing_change_address, is_send_all, fee_rate, max_fee, from_addresses).await?; let unsigned_transactions = - transactions.into_iter().map(|tx| PartiallySignedTransaction::from_unsigned(tx).encode_to_vec()).collect(); + self.create_unsigned_transactions(address, amount, from, use_existing_change_address, is_send_all, fee_policy).await?; + let unsigned_transactions = + unsigned_transactions.into_iter().map(|tx| PartiallySignedTransaction::from_unsigned(tx).encode_to_vec()).collect(); Ok(Response::new(CreateUnsignedTransactionsResponse { unsigned_transactions })) } @@ -93,14 +83,9 @@ impl Kaspawalletd for Service { // - New parameters like allow_parallel should be introduced // - Client behavior should be considered as they may expect sequential processing until the first error when sending batches async fn broadcast(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - let txs = deserialize_txs(request.transactions, request.is_domain, self.use_ecdsa())?; - let mut tx_ids: Vec = Vec::with_capacity(txs.len()); - for tx in txs { - let tx_id = - self.wallet().rpc_api().submit_transaction(tx, false).await.map_err(|e| Status::new(Code::Internal, e.to_string()))?; - tx_ids.push(tx_id.to_string()); - } + let BroadcastRequest { transactions, is_domain } = request.into_inner(); + let deserialized = deserialize_txs(transactions, is_domain, self.use_ecdsa())?; + let tx_ids = self.broadcast(deserialized).await?; Ok(Response::new(BroadcastResponse { tx_ids })) } @@ -128,88 +113,26 @@ impl Kaspawalletd for Service { Ok(Response::new(BroadcastResponse { tx_ids })) } - async fn send(&self, _request: Request) -> Result, Status> { - let acc = self.wallet().account().map_err(|err| Status::internal(err.to_string()))?; - if acc.minimum_signatures() != 1 { - return Err(Status::unimplemented("Only single signature wallets are supported")); - } - if acc.receive_address().map_err(|err| Status::internal(err.to_string()))?.version == Version::PubKeyECDSA { - return Err(Status::unimplemented("Ecdsa wallets are not supported yet")); - } - - // todo call unsigned tx and sign it to be consistent - - let data = _request.get_ref(); - let fee_rate_estimate = self.wallet().fee_rate_estimate().await.unwrap(); - let fee_rate = data.fee_policy.and_then(|policy| match policy.fee_policy.unwrap() { - FeePolicy::MaxFeeRate(rate) => Some(fee_rate_estimate.normal.feerate.min(rate)), - FeePolicy::ExactFeeRate(rate) => Some(rate), - _ => None, // TODO: we dont support maximum_amount policy so think if we should supply default fee_rate_estimate or just 1 on this case... - }); - let request = AccountsSendRequest { - account_id: self.descriptor().account_id, - wallet_secret: data.password.clone().into(), - payment_secret: None, - destination: PaymentDestination::PaymentOutputs(PaymentOutputs::from(( - Address::try_from(data.to_address.clone()).unwrap(), - data.amount, - ))), - fee_rate, - priority_fee_sompi: Fees::SenderPays(0), - payload: None, - }; - let result = self - .wallet() - .accounts_send(request) - .await - .map_err(|err| Status::new(tonic::Code::Internal, format!("Generator: {}", err)))?; - let final_transaction = result.final_transaction_id.unwrap().to_string(); - // todo return all transactions - let response = SendResponse { tx_ids: vec![final_transaction], signed_transactions: vec![] }; - Ok(Response::new(response)) + async fn send(&self, request: Request) -> Result, Status> { + let SendRequest { to_address, amount, password, from, use_existing_change_address, is_send_all, fee_policy } = + request.into_inner(); + let (signed_transactions, tx_ids) = + self.send(to_address, amount, password, from, use_existing_change_address, is_send_all, fee_policy).await?; + Ok(Response::new(SendResponse { tx_ids, signed_transactions })) } async fn sign(&self, request: Request) -> Result, Status> { - if self.use_ecdsa() { - return Err(Status::unimplemented("Ecdsa signing is not supported yet")); - } let SignRequest { unsigned_transactions, password } = request.into_inner(); - let account = self.wallet().account().map_err(|err| Status::internal(err.to_string()))?; - let txs = unsigned_transactions + let deserialized = unsigned_transactions .iter() .map(|tx| extract_tx(tx.as_slice(), self.use_ecdsa())) // todo convert directly to consensus::transaction .map(|r| r .and_then(|rtx| Transaction::try_from(rtx) - .map_err(|err| Status::internal(err.to_string())))) + .map_err(|err| Status::internal(err.to_string())))) .collect::, _>>()?; - let utxos = account.clone().get_utxos(None, None).await.map_err(|err| Status::internal(err.to_string()))?; - let signable_txs: Vec = txs - .into_iter() - .map(|tx| { - let utxos = tx - .inputs - .iter() - .map(|input| { - utxos - .iter() - .find(|utxo| utxo.outpoint != input.previous_outpoint) - .map(UtxoEntry::from) - .ok_or(Status::invalid_argument(format!("Wallet does not have mature utxo for input {input:?}"))) - }) - .collect::>()?; - Ok(SignableTransaction::with_entries(tx, utxos)) - }) - .collect::>()?; - let addresses: Vec<_> = account.utxo_context().addresses().iter().map(|addr| addr.as_ref().clone()).collect(); - let signer = Signer::new( - account.clone(), - account.prv_key_data(password.into()).await.map_err(|err| Status::internal(err.to_string()))?, - None, - ); - let _signed_txs = signable_txs.into_iter().map(|tx| signer.try_sign(tx, addresses.as_slice())); - // todo fill all required fields, serialize and return - todo!() + let signed_transactions = self.sign(deserialized, password).await?; + Ok(Response::new(SignResponse { signed_transactions })) } async fn get_version(&self, _request: Request) -> Result, Status> { diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index ade1522013..51074247fe 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -2,10 +2,11 @@ use fee_policy::FeePolicy; use futures_util::{select, FutureExt, TryStreamExt}; use kaspa_addresses::Prefix; use kaspa_consensus_core::constants::SOMPI_PER_KASPA; +use kaspa_consensus_core::tx::{SignableTransaction, Transaction, UtxoEntry}; use kaspa_rpc_core::RpcTransaction; use kaspa_wallet_core::api::NewAddressKind; use kaspa_wallet_core::prelude::{PaymentDestination, PaymentOutput, PaymentOutputs}; -use kaspa_wallet_core::tx::{Fees, Generator, GeneratorSettings}; +use kaspa_wallet_core::tx::{Fees, Generator, GeneratorSettings, Signer, SignerT}; use kaspa_wallet_core::utxo::UtxoEntryReference; use kaspa_wallet_core::{ api::WalletApi, @@ -17,7 +18,7 @@ use kaspa_wallet_grpc_core::kaspawalletd; use kaspa_wallet_grpc_core::kaspawalletd::fee_policy; use std::sync::{Arc, Mutex}; use tokio::sync::oneshot; -use tonic::Status; +use tonic::{Code, Status}; pub struct Service { wallet: Arc, @@ -61,6 +62,93 @@ impl Service { Service { wallet, shutdown_sender: Arc::new(Mutex::new(Some(shutdown_sender))), ecdsa } } + pub async fn sign(&self, unsigned_transactions: Vec, password: String) -> Result, Status> { + if self.use_ecdsa() { + return Err(Status::unimplemented("Ecdsa signing is not supported yet")); + } + let account = self.wallet().account().map_err(|err| Status::internal(err.to_string()))?; + let utxos = account.clone().get_utxos(None, None).await.map_err(|err| Status::internal(err.to_string()))?; + let signable_txs: Vec = unsigned_transactions + .into_iter() + .map(|tx| { + let utxos = tx + .inputs + .iter() + .map(|input| { + utxos + .iter() + .find(|utxo| utxo.outpoint != input.previous_outpoint) + .map(UtxoEntry::from) + .ok_or(Status::invalid_argument(format!("Wallet does not have mature utxo for input {input:?}"))) + }) + .collect::>()?; + Ok(SignableTransaction::with_entries(tx, utxos)) + }) + .collect::>()?; + let addresses: Vec<_> = account.utxo_context().addresses().iter().map(|addr| addr.as_ref().clone()).collect(); + let signer = Signer::new( + account.clone(), + account.prv_key_data(password.into()).await.map_err(|err| Status::internal(err.to_string()))?, + None, + ); + let _signed_txs = signable_txs.into_iter().map(|tx| signer.try_sign(tx, addresses.as_slice())); + // todo fill all required fields, serialize and return + todo!() + } + + pub async fn create_unsigned_transactions( + &self, + address: String, + amount: u64, + from: Vec, + use_existing_change_address: bool, + is_send_all: bool, + fee_policy: Option, + ) -> Result, Status> { + let to_address = Address::try_from(address).map_err(|err| Status::invalid_argument(err.to_string()))?; + let (fee_rate, max_fee) = self.calculate_fee_limits(fee_policy).await?; + let from_addresses = from + .iter() + .map(|a| Address::try_from(a.as_str())) + .collect::, _>>() + .map_err(|err| Status::invalid_argument(err.to_string()))?; + let transactions = + self.unsigned_txs(to_address, amount, use_existing_change_address, is_send_all, fee_rate, max_fee, from_addresses).await?; + Ok(transactions) + } + + pub async fn broadcast(&self, transactions: Vec) -> Result, Status> { + let mut tx_ids: Vec = Vec::with_capacity(transactions.len()); + for tx in transactions { + let tx_id = + self.wallet().rpc_api().submit_transaction(tx, false).await.map_err(|e| Status::new(Code::Internal, e.to_string()))?; + tx_ids.push(tx_id.to_string()); + } + Ok(tx_ids) + } + + pub async fn send( + &self, + to_address: String, + amount: u64, + password: String, + from: Vec, + use_existing_change_address: bool, + is_send_all: bool, + fee_policy: Option, + ) -> Result<(Vec>, Vec), Status> { + let unsigned_transactions = + self.create_unsigned_transactions(to_address, amount, from, use_existing_change_address, is_send_all, fee_policy).await?; + let unsigned_transactions = unsigned_transactions + .into_iter() + .map(|tx| tx.try_into().map_err(|_e| Status::invalid_argument("Invalid unsigned transaction"))) + .collect::, _>>(); + let _signed_transactions = self.sign(unsigned_transactions?, password).await?; + // let tx_ids = self.broadcast(signed_transactions.clone(), false).await?; + // Ok((signed_transactions, tx_ids)) + todo!() + } + pub async fn calculate_fee_limits(&self, fee_policy: Option) -> Result<(f64, u64), Status> { let fee_policy = fee_policy.and_then(|fee_policy| fee_policy.fee_policy); const MIN_FEE_RATE: f64 = 1.0;