From b126a61a6ee97e7a1f8c5dd097ba00f15a3c0a11 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Fri, 20 Jun 2025 20:48:04 +0300 Subject: [PATCH 1/9] feat: update send logic (reuse create_unsigned_transaction, sign and broadcast functions) --- wallet/grpc/core/src/convert.rs | 17 +++++++++- wallet/grpc/server/src/lib.rs | 58 +++++++++------------------------ 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/wallet/grpc/core/src/convert.rs b/wallet/grpc/core/src/convert.rs index 670c8b02ea..ffa4381180 100644 --- a/wallet/grpc/core/src/convert.rs +++ b/wallet/grpc/core/src/convert.rs @@ -1,4 +1,6 @@ -use crate::kaspawalletd::{Outpoint, ScriptPublicKey, UtxoEntry, UtxosByAddressesEntry}; +use crate::kaspawalletd::{ + CreateUnsignedTransactionsRequest, Outpoint, ScriptPublicKey, SendRequest, UtxoEntry, UtxosByAddressesEntry, +}; use crate::protoserialization; use kaspa_bip32::secp256k1::PublicKey; use kaspa_bip32::{DerivationPath, Error, ExtendedKey, ExtendedPublicKey}; @@ -318,3 +320,16 @@ impl From for protoserialization::SubnetworkId { Self { bytes: bts.to_vec() } } } + +impl From<&SendRequest> for CreateUnsignedTransactionsRequest { + fn from(value: &SendRequest) -> Self { + Self { + address: value.to_address.clone(), + amount: value.amount, + from: value.from.clone(), + use_existing_change_address: value.use_existing_change_address, + is_send_all: value.is_send_all, + fee_policy: value.fee_policy, + } + } +} diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index 3dc57f7c72..bd6c591019 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -1,17 +1,15 @@ pub mod service; -use kaspa_addresses::Version; use kaspa_consensus_core::tx::{SignableTransaction, Transaction, UtxoEntry}; 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, @@ -128,45 +126,19 @@ 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 request = request.into_inner(); + let create_unsigned_req: Request = Request::new((&request).into()); + let unsigned_txs = self.create_unsigned_transactions(create_unsigned_req).await?.into_inner(); + let signed_txs = self + .sign(Request::new(SignRequest { unsigned_transactions: unsigned_txs.unsigned_transactions, password: request.password })) + .await? + .into_inner(); + let tx_ids = self + .broadcast(Request::new(BroadcastRequest { is_domain: false, transactions: signed_txs.signed_transactions.clone() })) + .await? + .into_inner(); + Ok(Response::new(SendResponse { tx_ids: tx_ids.tx_ids, signed_transactions: signed_txs.signed_transactions })) } async fn sign(&self, request: Request) -> Result, Status> { From e3ec436a3a8ac3b6ff9f1390ce3b897f04ae57dc Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Mon, 21 Jul 2025 10:28:07 +0300 Subject: [PATCH 2/9] feat: move logic into service layer --- wallet/grpc/server/src/lib.rs | 84 ++++--------------------- wallet/grpc/server/src/service.rs | 100 +++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index bd6c591019..b12b90c263 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -1,13 +1,11 @@ pub mod service; -use kaspa_consensus_core::tx::{SignableTransaction, Transaction, UtxoEntry}; use kaspa_wallet_core::api::WalletApi; -use kaspa_wallet_core::tx::{Signer, SignerT}; use kaspa_wallet_core::{ api::{AccountsGetUtxosRequest, NewAddressKind}, prelude::Address, }; -use kaspa_wallet_grpc_core::convert::{deserialize_txs, extract_tx}; +use kaspa_wallet_grpc_core::convert::deserialize_txs; use kaspa_wallet_grpc_core::kaspawalletd::{ kaspawalletd_server::Kaspawalletd, BroadcastRequest, BroadcastResponse, BumpFeeRequest, BumpFeeResponse, CreateUnsignedTransactionsRequest, CreateUnsignedTransactionsResponse, GetBalanceRequest, GetBalanceResponse, @@ -15,7 +13,6 @@ use kaspa_wallet_grpc_core::kaspawalletd::{ NewAddressResponse, SendRequest, SendResponse, ShowAddressesRequest, ShowAddressesResponse, ShutdownRequest, ShutdownResponse, SignRequest, SignResponse, }; -use kaspa_wallet_grpc_core::protoserialization::PartiallySignedTransaction; use service::Service; use tonic::{Code, Request, Response, Status}; @@ -49,17 +46,8 @@ 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?; Ok(Response::new(CreateUnsignedTransactionsResponse { unsigned_transactions })) } @@ -91,14 +79,8 @@ 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 tx_ids = self.broadcast(transactions, is_domain).await?; Ok(Response::new(BroadcastResponse { tx_ids })) } @@ -127,61 +109,17 @@ impl Kaspawalletd for Service { } async fn send(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - let create_unsigned_req: Request = Request::new((&request).into()); - let unsigned_txs = self.create_unsigned_transactions(create_unsigned_req).await?.into_inner(); - let signed_txs = self - .sign(Request::new(SignRequest { unsigned_transactions: unsigned_txs.unsigned_transactions, password: request.password })) - .await? - .into_inner(); - let tx_ids = self - .broadcast(Request::new(BroadcastRequest { is_domain: false, transactions: signed_txs.signed_transactions.clone() })) - .await? - .into_inner(); - Ok(Response::new(SendResponse { tx_ids: tx_ids.tx_ids, signed_transactions: signed_txs.signed_transactions })) + 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 - .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())))) - .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(unsigned_transactions, 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..7324d665ac 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, @@ -13,11 +14,13 @@ use kaspa_wallet_core::{ prelude::{AccountDescriptor, Address}, wallet::Wallet, }; +use kaspa_wallet_grpc_core::convert::{deserialize_txs, extract_tx}; use kaspa_wallet_grpc_core::kaspawalletd; use kaspa_wallet_grpc_core::kaspawalletd::fee_policy; +use kaspa_wallet_grpc_core::protoserialization::PartiallySignedTransaction; use std::sync::{Arc, Mutex}; use tokio::sync::oneshot; -use tonic::Status; +use tonic::{Code, Status}; pub struct Service { wallet: Arc, @@ -61,6 +64,99 @@ 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 txs = 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())))) + .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!() + } + + 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?; + let unsigned_transactions: Vec> = + transactions.into_iter().map(|tx| PartiallySignedTransaction::from_unsigned(tx).encode_to_vec()).collect(); + Ok(unsigned_transactions) + } + + pub async fn broadcast(&self, transactions: Vec>, is_domain: bool) -> Result, Status> { + let txs = deserialize_txs(transactions, 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()); + } + 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 signed_transactions = self.sign(unsigned_transactions, password).await?; + let tx_ids = self.broadcast(signed_transactions.clone(), false).await?; + Ok((signed_transactions, tx_ids)) + } + 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; From 34978505a6171fb9e124cc00a3d299608257396a Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Mon, 21 Jul 2025 10:30:44 +0300 Subject: [PATCH 3/9] fix! --- wallet/grpc/core/src/convert.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/wallet/grpc/core/src/convert.rs b/wallet/grpc/core/src/convert.rs index ffa4381180..c812062f3c 100644 --- a/wallet/grpc/core/src/convert.rs +++ b/wallet/grpc/core/src/convert.rs @@ -1,5 +1,5 @@ use crate::kaspawalletd::{ - CreateUnsignedTransactionsRequest, Outpoint, ScriptPublicKey, SendRequest, UtxoEntry, UtxosByAddressesEntry, + Outpoint, ScriptPublicKey, UtxoEntry, UtxosByAddressesEntry, }; use crate::protoserialization; use kaspa_bip32::secp256k1::PublicKey; @@ -320,16 +320,3 @@ impl From for protoserialization::SubnetworkId { Self { bytes: bts.to_vec() } } } - -impl From<&SendRequest> for CreateUnsignedTransactionsRequest { - fn from(value: &SendRequest) -> Self { - Self { - address: value.to_address.clone(), - amount: value.amount, - from: value.from.clone(), - use_existing_change_address: value.use_existing_change_address, - is_send_all: value.is_send_all, - fee_policy: value.fee_policy, - } - } -} From 1399143f924892ab91c7378a743d9e3fa562af87 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Mon, 21 Jul 2025 16:58:00 +0300 Subject: [PATCH 4/9] fix! --- wallet/grpc/core/src/convert.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wallet/grpc/core/src/convert.rs b/wallet/grpc/core/src/convert.rs index c812062f3c..670c8b02ea 100644 --- a/wallet/grpc/core/src/convert.rs +++ b/wallet/grpc/core/src/convert.rs @@ -1,6 +1,4 @@ -use crate::kaspawalletd::{ - Outpoint, ScriptPublicKey, UtxoEntry, UtxosByAddressesEntry, -}; +use crate::kaspawalletd::{Outpoint, ScriptPublicKey, UtxoEntry, UtxosByAddressesEntry}; use crate::protoserialization; use kaspa_bip32::secp256k1::PublicKey; use kaspa_bip32::{DerivationPath, Error, ExtendedKey, ExtendedPublicKey}; From 6d1fccfd2a5ddfa0242a81dfd05032a5ad3d8efa Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Mon, 21 Jul 2025 17:13:07 +0300 Subject: [PATCH 5/9] fix! --- wallet/grpc/server/src/lib.rs | 13 +++++++++++-- wallet/grpc/server/src/service.rs | 25 +++++++++---------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index b12b90c263..b8bed8ef00 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -1,11 +1,12 @@ pub mod service; +use kaspa_consensus_core::tx::Transaction; use kaspa_wallet_core::api::WalletApi; use kaspa_wallet_core::{ api::{AccountsGetUtxosRequest, NewAddressKind}, prelude::Address, }; -use kaspa_wallet_grpc_core::convert::deserialize_txs; +use kaspa_wallet_grpc_core::convert::{deserialize_txs, extract_tx}; use kaspa_wallet_grpc_core::kaspawalletd::{ kaspawalletd_server::Kaspawalletd, BroadcastRequest, BroadcastResponse, BumpFeeRequest, BumpFeeResponse, CreateUnsignedTransactionsRequest, CreateUnsignedTransactionsResponse, GetBalanceRequest, GetBalanceResponse, @@ -118,7 +119,15 @@ impl Kaspawalletd for Service { async fn sign(&self, request: Request) -> Result, Status> { let SignRequest { unsigned_transactions, password } = request.into_inner(); - let signed_transactions = self.sign(unsigned_transactions, password).await?; + 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())))) + .collect::, _>>()?; + let signed_transactions = self.sign(deserialized, password).await?; Ok(Response::new(SignResponse { signed_transactions })) } diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index 7324d665ac..fae80b35b8 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -14,7 +14,7 @@ use kaspa_wallet_core::{ prelude::{AccountDescriptor, Address}, wallet::Wallet, }; -use kaspa_wallet_grpc_core::convert::{deserialize_txs, extract_tx}; +use kaspa_wallet_grpc_core::convert::deserialize_txs; use kaspa_wallet_grpc_core::kaspawalletd; use kaspa_wallet_grpc_core::kaspawalletd::fee_policy; use kaspa_wallet_grpc_core::protoserialization::PartiallySignedTransaction; @@ -64,21 +64,13 @@ 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> { + 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 txs = 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())))) - .collect::, _>>()?; let utxos = account.clone().get_utxos(None, None).await.map_err(|err| Status::internal(err.to_string()))?; - let signable_txs: Vec = txs + let signable_txs: Vec = unsigned_transactions .into_iter() .map(|tx| { let utxos = tx @@ -144,17 +136,18 @@ impl Service { &self, to_address: String, amount: u64, - password: String, + _password: String, from: Vec, use_existing_change_address: bool, is_send_all: bool, fee_policy: Option, ) -> Result<(Vec>, Vec), Status> { - let unsigned_transactions = + let _unsigned_transactions = self.create_unsigned_transactions(to_address, amount, from, use_existing_change_address, is_send_all, fee_policy).await?; - let signed_transactions = self.sign(unsigned_transactions, password).await?; - let tx_ids = self.broadcast(signed_transactions.clone(), false).await?; - Ok((signed_transactions, tx_ids)) + // 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> { From ad8241633fe9cf2079c11751fd6382a4c49deb62 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Wed, 23 Jul 2025 18:10:27 +0300 Subject: [PATCH 6/9] fix! --- wallet/grpc/server/src/lib.rs | 3 +++ wallet/grpc/server/src/service.rs | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index b8bed8ef00..12f4300b39 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -14,6 +14,7 @@ use kaspa_wallet_grpc_core::kaspawalletd::{ NewAddressResponse, SendRequest, SendResponse, ShowAddressesRequest, ShowAddressesResponse, ShutdownRequest, ShutdownResponse, SignRequest, SignResponse, }; +use kaspa_wallet_grpc_core::protoserialization::PartiallySignedTransaction; use service::Service; use tonic::{Code, Request, Response, Status}; @@ -49,6 +50,8 @@ impl Kaspawalletd for Service { request.into_inner(); let unsigned_transactions = 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 })) } diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index fae80b35b8..95cd5ffdd2 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -17,7 +17,6 @@ use kaspa_wallet_core::{ use kaspa_wallet_grpc_core::convert::deserialize_txs; use kaspa_wallet_grpc_core::kaspawalletd; use kaspa_wallet_grpc_core::kaspawalletd::fee_policy; -use kaspa_wallet_grpc_core::protoserialization::PartiallySignedTransaction; use std::sync::{Arc, Mutex}; use tokio::sync::oneshot; use tonic::{Code, Status}; @@ -106,7 +105,7 @@ impl Service { use_existing_change_address: bool, is_send_all: bool, fee_policy: Option, - ) -> Result>, Status> { + ) -> 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 @@ -116,9 +115,7 @@ impl Service { .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: Vec> = - transactions.into_iter().map(|tx| PartiallySignedTransaction::from_unsigned(tx).encode_to_vec()).collect(); - Ok(unsigned_transactions) + Ok(transactions) } pub async fn broadcast(&self, transactions: Vec>, is_domain: bool) -> Result, Status> { From 7e5d9f8e75168a3913bfd1c7dc2dcf6eedc8de88 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Wed, 23 Jul 2025 19:34:08 +0300 Subject: [PATCH 7/9] fix: refactor transaction broadcasting logic and remove unused import --- wallet/grpc/server/src/lib.rs | 3 ++- wallet/grpc/server/src/service.rs | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs index 12f4300b39..90b13b6af7 100644 --- a/wallet/grpc/server/src/lib.rs +++ b/wallet/grpc/server/src/lib.rs @@ -84,7 +84,8 @@ impl Kaspawalletd for Service { // - 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 BroadcastRequest { transactions, is_domain } = request.into_inner(); - let tx_ids = self.broadcast(transactions, is_domain).await?; + let deserialized = deserialize_txs(transactions, is_domain, self.use_ecdsa())?; + let tx_ids = self.broadcast(deserialized).await?; Ok(Response::new(BroadcastResponse { tx_ids })) } diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index 95cd5ffdd2..070c883390 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -14,7 +14,6 @@ use kaspa_wallet_core::{ prelude::{AccountDescriptor, Address}, wallet::Wallet, }; -use kaspa_wallet_grpc_core::convert::deserialize_txs; use kaspa_wallet_grpc_core::kaspawalletd; use kaspa_wallet_grpc_core::kaspawalletd::fee_policy; use std::sync::{Arc, Mutex}; @@ -118,10 +117,9 @@ impl Service { Ok(transactions) } - pub async fn broadcast(&self, transactions: Vec>, is_domain: bool) -> Result, Status> { - let txs = deserialize_txs(transactions, is_domain, self.use_ecdsa())?; - let mut tx_ids: Vec = Vec::with_capacity(txs.len()); - for tx in txs { + 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()); From d76ff24db7b6ee1c6f1cc7daf6dc0e55b1ffae71 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Wed, 23 Jul 2025 19:40:32 +0300 Subject: [PATCH 8/9] fix: update transaction signing flow with proper error handling --- wallet/grpc/server/src/service.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index 070c883390..12cf4257e6 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -131,15 +131,19 @@ impl Service { &self, to_address: String, amount: u64, - _password: String, + password: String, from: Vec, use_existing_change_address: bool, is_send_all: bool, fee_policy: Option, ) -> Result<(Vec>, Vec), Status> { - let _unsigned_transactions = + let unsigned_transactions = self.create_unsigned_transactions(to_address, amount, from, use_existing_change_address, is_send_all, fee_policy).await?; - // let signed_transactions = self.sign(unsigned_transactions, password).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!() From 887c794f3739b2cf32a2d3842e3c68d121e7a0c3 Mon Sep 17 00:00:00 2001 From: Nikolay Zelenev Date: Fri, 25 Jul 2025 16:51:45 +0300 Subject: [PATCH 9/9] fix: update sign method to use RpcTransaction type for consistent return format --- wallet/grpc/server/src/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/grpc/server/src/service.rs b/wallet/grpc/server/src/service.rs index 12cf4257e6..51074247fe 100644 --- a/wallet/grpc/server/src/service.rs +++ b/wallet/grpc/server/src/service.rs @@ -62,7 +62,7 @@ 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> { + 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")); }