diff --git a/Cargo.lock b/Cargo.lock index 440f2f256f..d7e1f78c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3373,6 +3373,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kaspa-wallet-grpc-core" +version = "0.16.0" +dependencies = [ + "prost", + "tonic", + "tonic-build", +] + +[[package]] +name = "kaspa-wallet-grpc-server" +version = "0.16.0" +dependencies = [ + "kaspa-wallet-grpc-core", + "tokio", + "tonic", +] + [[package]] name = "kaspa-wallet-keys" version = "0.16.0" diff --git a/Cargo.toml b/Cargo.toml index 7ab0a2579d..f6652c1df9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ members = [ "core", "wallet/macros", "wallet/core", + "wallet/grpc/core", + "wallet/grpc/server", "wallet/native", "wallet/wasm", "wallet/bip32", @@ -125,6 +127,8 @@ kaspa-wallet-cli-wasm = { version = "0.16.0", path = "wallet/wasm" } kaspa-wallet-keys = { version = "0.16.0", path = "wallet/keys" } kaspa-wallet-pskt = { version = "0.16.0", path = "wallet/pskt" } kaspa-wallet-core = { version = "0.16.0", path = "wallet/core" } +kaspa-wallet-grpc-core = { version = "0.16.0", path = "wallet/grpc/core" } +kaspa-wallet-grpc-server = { version = "0.16.0", path = "wallet/grpc/server" } kaspa-wallet-macros = { version = "0.16.0", path = "wallet/macros" } kaspa-wasm = { version = "0.16.0", path = "wasm" } kaspa-wasm-core = { version = "0.16.0", path = "wasm/core" } diff --git a/wallet/grpc/core/Cargo.toml b/wallet/grpc/core/Cargo.toml new file mode 100644 index 0000000000..5d0669abb4 --- /dev/null +++ b/wallet/grpc/core/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "kaspa-wallet-grpc-core" +rust-version.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +include.workspace = true + +[dependencies] +tonic.workspace = true +prost.workspace = true + +[lints] +workspace = true + +[build-dependencies] +tonic-build = { workspace = true, features = ["prost"] } diff --git a/wallet/grpc/core/build.rs b/wallet/grpc/core/build.rs new file mode 100644 index 0000000000..dcd8139b89 --- /dev/null +++ b/wallet/grpc/core/build.rs @@ -0,0 +1,9 @@ +use std::{io::Result, path::PathBuf}; + +fn main() -> Result<()> { + let proto_file = "./proto/kaspawalletd.proto"; + let proto_dir = PathBuf::from("./proto"); + tonic_build::configure().build_server(true).build_client(true).compile_protos(&[proto_file], &[proto_dir])?; + println!("cargo:rerun-if-changed={}", proto_file); + Ok(()) +} diff --git a/wallet/grpc/core/proto/kaspawalletd.proto b/wallet/grpc/core/proto/kaspawalletd.proto new file mode 100644 index 0000000000..918a6d5e1b --- /dev/null +++ b/wallet/grpc/core/proto/kaspawalletd.proto @@ -0,0 +1,151 @@ +syntax = "proto3"; + +option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"; +package kaspawalletd; + +service kaspawalletd { + rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {} + rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest) + returns (GetExternalSpendableUTXOsResponse) {} + rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest) + returns (CreateUnsignedTransactionsResponse) {} + rpc ShowAddresses(ShowAddressesRequest) returns (ShowAddressesResponse) {} + rpc NewAddress(NewAddressRequest) returns (NewAddressResponse) {} + rpc Shutdown(ShutdownRequest) returns (ShutdownResponse) {} + rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) {} + // BroadcastReplacement assumes that all transactions depend on the first one + rpc BroadcastReplacement(BroadcastRequest) returns (BroadcastResponse) {} + // Since SendRequest contains a password - this command should only be used on + // a trusted or secure connection + rpc Send(SendRequest) returns (SendResponse) {} + // Since SignRequest contains a password - this command should only be used on + // a trusted or secure connection + rpc Sign(SignRequest) returns (SignResponse) {} + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {} + rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse) {} +} + +message GetBalanceRequest {} + +message GetBalanceResponse { + uint64 available = 1; + uint64 pending = 2; + repeated AddressBalances addressBalances = 3; +} + +message AddressBalances { + string address = 1; + uint64 available = 2; + uint64 pending = 3; +} + +message FeePolicy { + oneof feePolicy { + double maxFeeRate = 6; + double exactFeeRate = 7; + uint64 maxFee = 8; + } +} + +message CreateUnsignedTransactionsRequest { + string address = 1; + uint64 amount = 2; + repeated string from = 3; + bool useExistingChangeAddress = 4; + bool isSendAll = 5; + FeePolicy feePolicy = 6; +} + +message CreateUnsignedTransactionsResponse { + repeated bytes unsignedTransactions = 1; +} + +message ShowAddressesRequest {} + +message ShowAddressesResponse { repeated string address = 1; } + +message NewAddressRequest {} + +message NewAddressResponse { string address = 1; } + +message BroadcastRequest { + bool isDomain = 1; + repeated bytes transactions = 2; +} + +message BroadcastResponse { repeated string txIds = 1; } + +message ShutdownRequest {} + +message ShutdownResponse {} + +message Outpoint { + string transactionId = 1; + uint32 index = 2; +} + +message UtxosByAddressesEntry { + string address = 1; + Outpoint outpoint = 2; + UtxoEntry utxoEntry = 3; +} + +message ScriptPublicKey { + uint32 version = 1; + string scriptPublicKey = 2; +} + +message UtxoEntry { + uint64 amount = 1; + ScriptPublicKey scriptPublicKey = 2; + uint64 blockDaaScore = 3; + bool isCoinbase = 4; +} + +message GetExternalSpendableUTXOsRequest { string address = 1; } + +message GetExternalSpendableUTXOsResponse { + repeated UtxosByAddressesEntry Entries = 1; +} +// Since SendRequest contains a password - this command should only be used on a +// trusted or secure connection +message SendRequest { + string toAddress = 1; + uint64 amount = 2; + string password = 3; + repeated string from = 4; + bool useExistingChangeAddress = 5; + bool isSendAll = 6; + FeePolicy feePolicy = 7; +} + +message SendResponse { + repeated string txIds = 1; + repeated bytes signedTransactions = 2; +} + +// Since SignRequest contains a password - this command should only be used on a +// trusted or secure connection +message SignRequest { + repeated bytes unsignedTransactions = 1; + string password = 2; +} + +message SignResponse { repeated bytes signedTransactions = 1; } + +message GetVersionRequest {} + +message GetVersionResponse { string version = 1; } + +message BumpFeeRequest { + string password = 1; + repeated string from = 2; + bool useExistingChangeAddress = 3; + FeePolicy feePolicy = 4; + string txId = 5; +} + +message BumpFeeResponse { + repeated bytes transactions = 1; + repeated string txIds = 2; +} diff --git a/wallet/grpc/core/src/lib.rs b/wallet/grpc/core/src/lib.rs new file mode 100644 index 0000000000..da8f4d7939 --- /dev/null +++ b/wallet/grpc/core/src/lib.rs @@ -0,0 +1,4 @@ +// Include the generated proto types +pub mod kaspawalletd { + include!(concat!(env!("OUT_DIR"), "/kaspawalletd.rs")); +} diff --git a/wallet/grpc/server/Cargo.toml b/wallet/grpc/server/Cargo.toml new file mode 100644 index 0000000000..fe4b3e4832 --- /dev/null +++ b/wallet/grpc/server/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "kaspa-wallet-grpc-server" +rust-version.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +include.workspace = true + +[dependencies] +kaspa-wallet-grpc-core.workspace = true +tonic.workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "test-util"] } + +[lints] +workspace = true diff --git a/wallet/grpc/server/src/lib.rs b/wallet/grpc/server/src/lib.rs new file mode 100644 index 0000000000..c8d49faa5c --- /dev/null +++ b/wallet/grpc/server/src/lib.rs @@ -0,0 +1,123 @@ +use kaspa_wallet_grpc_core::kaspawalletd::{ + kaspawalletd_server::Kaspawalletd, BroadcastRequest, BroadcastResponse, BumpFeeRequest, BumpFeeResponse, + CreateUnsignedTransactionsRequest, CreateUnsignedTransactionsResponse, GetBalanceRequest, GetBalanceResponse, + GetExternalSpendableUtxOsRequest, GetExternalSpendableUtxOsResponse, GetVersionRequest, GetVersionResponse, NewAddressRequest, + NewAddressResponse, SendRequest, SendResponse, ShowAddressesRequest, ShowAddressesResponse, ShutdownRequest, ShutdownResponse, + SignRequest, SignResponse, +}; +use tonic::{Request, Response, Status}; + +#[derive(Debug, Default)] +pub struct KaspaWalletService { + // Add your service state here +} + +#[tonic::async_trait] +impl Kaspawalletd for KaspaWalletService { + async fn get_balance(&self, _request: Request) -> Result, Status> { + let response = GetBalanceResponse { available: 0, pending: 0, address_balances: vec![] }; + Ok(Response::new(response)) + } + + async fn get_external_spendable_utx_os( + &self, + _request: Request, + ) -> Result, Status> { + let response = GetExternalSpendableUtxOsResponse { entries: vec![] }; + Ok(Response::new(response)) + } + + async fn create_unsigned_transactions( + &self, + _request: Request, + ) -> Result, Status> { + let response = CreateUnsignedTransactionsResponse { unsigned_transactions: vec![] }; + Ok(Response::new(response)) + } + + async fn show_addresses(&self, _request: Request) -> Result, Status> { + let response = ShowAddressesResponse { address: vec![] }; + Ok(Response::new(response)) + } + + async fn new_address(&self, _request: Request) -> Result, Status> { + let response = NewAddressResponse { address: "".to_string() }; + Ok(Response::new(response)) + } + + async fn shutdown(&self, _request: Request) -> Result, Status> { + let response = ShutdownResponse {}; + Ok(Response::new(response)) + } + + async fn broadcast(&self, _request: Request) -> Result, Status> { + let response = BroadcastResponse { tx_ids: vec![] }; + Ok(Response::new(response)) + } + + async fn broadcast_replacement(&self, _request: Request) -> Result, Status> { + let response = BroadcastResponse { tx_ids: vec![] }; + Ok(Response::new(response)) + } + + async fn send(&self, _request: Request) -> Result, Status> { + let response = SendResponse { tx_ids: vec![], signed_transactions: vec![] }; + Ok(Response::new(response)) + } + + async fn sign(&self, _request: Request) -> Result, Status> { + let response = SignResponse { signed_transactions: vec![] }; + Ok(Response::new(response)) + } + + async fn get_version(&self, _request: Request) -> Result, Status> { + let response = GetVersionResponse { version: "".to_string() }; + Ok(Response::new(response)) + } + + async fn bump_fee(&self, _request: Request) -> Result, Status> { + let response = BumpFeeResponse { transactions: vec![], tx_ids: vec![] }; + Ok(Response::new(response)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use kaspa_wallet_grpc_core::kaspawalletd::{ + kaspawalletd_server::KaspawalletdServer, GetBalanceRequest, GetVersionRequest, NewAddressRequest, + }; + use std::time::Duration; + use tonic::transport::Server; + + #[tokio::test] + async fn test_server_basic_requests() { + // Start server + let addr = "[::1]:50051".parse().unwrap(); + let service = KaspaWalletService::default(); + + let server_handle = tokio::spawn(async move { + Server::builder().add_service(KaspawalletdServer::new(service)).serve(addr).await.unwrap(); + }); + tokio::time::sleep(Duration::from_secs(1)).await; // wait until server starts + // Create client + let mut client = kaspa_wallet_grpc_core::kaspawalletd::kaspawalletd_client::KaspawalletdClient::connect("http://[::1]:50051") + .await + .unwrap(); + + // Test GetBalance + let balance = client.get_balance(GetBalanceRequest {}).await.unwrap(); + assert_eq!(balance.get_ref().available, 0); + assert_eq!(balance.get_ref().pending, 0); + + // Test GetVersion + let version = client.get_version(GetVersionRequest {}).await.unwrap(); + assert_eq!(version.get_ref().version, ""); + + // Test NewAddress + let address = client.new_address(NewAddressRequest {}).await.unwrap(); + assert_eq!(address.get_ref().address, ""); + + server_handle.abort(); + } +}