Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 19 additions & 96 deletions wallet/grpc/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -51,17 +48,10 @@ impl Kaspawalletd for Service {
) -> Result<Response<CreateUnsignedTransactionsResponse>, 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::<Result<Vec<_>, _>>()
.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 }))
}

Expand Down Expand Up @@ -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<BroadcastRequest>) -> Result<Response<BroadcastResponse>, Status> {
let request = request.into_inner();
let txs = deserialize_txs(request.transactions, request.is_domain, self.use_ecdsa())?;
let mut tx_ids: Vec<String> = 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 }))
}

Expand Down Expand Up @@ -128,88 +113,26 @@ impl Kaspawalletd for Service {
Ok(Response::new(BroadcastResponse { tx_ids }))
}

async fn send(&self, _request: Request<SendRequest>) -> Result<Response<SendResponse>, 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<SendRequest>) -> Result<Response<SendResponse>, 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<SignRequest>) -> Result<Response<SignResponse>, 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::<Result<Vec<_>, _>>()?;
let utxos = account.clone().get_utxos(None, None).await.map_err(|err| Status::internal(err.to_string()))?;
let signable_txs: Vec<SignableTransaction> = 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::<Result<_, Status>>()?;
Ok(SignableTransaction::with_entries(tx, utxos))
})
.collect::<Result<_, Status>>()?;
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<GetVersionRequest>) -> Result<Response<GetVersionResponse>, Status> {
Expand Down
92 changes: 90 additions & 2 deletions wallet/grpc/server/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Wallet>,
Expand Down Expand Up @@ -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<Transaction>, password: String) -> Result<Vec<RpcTransaction>, 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<SignableTransaction> = 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::<Result<_, Status>>()?;
Ok(SignableTransaction::with_entries(tx, utxos))
})
.collect::<Result<_, Status>>()?;
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<String>,
use_existing_change_address: bool,
is_send_all: bool,
fee_policy: Option<kaspawalletd::FeePolicy>,
) -> Result<Vec<RpcTransaction>, 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::<Result<Vec<_>, _>>()
.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<RpcTransaction>) -> Result<Vec<String>, Status> {
let mut tx_ids: Vec<String> = 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<String>,
use_existing_change_address: bool,
is_send_all: bool,
fee_policy: Option<kaspawalletd::FeePolicy>,
) -> Result<(Vec<Vec<u8>>, Vec<String>), 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::<Result<Vec<_>, _>>();
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<kaspawalletd::FeePolicy>) -> Result<(f64, u64), Status> {
let fee_policy = fee_policy.and_then(|fee_policy| fee_policy.fee_policy);
const MIN_FEE_RATE: f64 = 1.0;
Expand Down
Loading