diff --git a/wallet/core/src/account/mod.rs b/wallet/core/src/account/mod.rs index 70cca0ad12..e6b48dfec2 100644 --- a/wallet/core/src/account/mod.rs +++ b/wallet/core/src/account/mod.rs @@ -19,6 +19,7 @@ pub use variants::*; use crate::derivation::build_derivate_paths; use crate::derivation::AddressDerivationManagerTrait; use crate::imports::*; +use crate::message::{verify_message, PersonalMessage, SignMessageOptions}; use crate::storage::account::AccountSettings; use crate::storage::AccountMetadata; use crate::storage::{PrvKeyData, PrvKeyDataId}; @@ -30,6 +31,7 @@ use kaspa_bip32::{ChildNumber, ExtendedPrivateKey, PrivateKey}; use kaspa_consensus_client::UtxoEntry; use kaspa_consensus_client::UtxoEntryReference; use kaspa_wallet_keys::derivation::gen0::WalletDerivationManagerV0; +use secp256k1::PublicKey; use workflow_core::abortable::Abortable; /// Notification callback type used by [`Account::sweep`] and [`Account::send`]. @@ -506,6 +508,41 @@ pub trait Account: AnySync + Send + Sync + 'static { Ok(ids) } + async fn sign_message( + self: Arc, + message: &str, + address: &Address, + wallet_secret: Secret, + payment_secret: Option, + no_aux_rand: bool, + ) -> Result<(String, PublicKey)> { + let keydata = self.prv_key_data(wallet_secret).await?; + let signer = Arc::new(Signer::new(self.clone().as_dyn_arc(), keydata, payment_secret)); + let options = SignMessageOptions { no_aux_rand }; + signer.sign_message(message, address, options).await + } + + async fn verify_message( + self: Arc, + message: &str, + signature: &str, + address: &Address, + wallet_secret: Secret, + payment_secret: Option, + ) -> Result { + let pm = PersonalMessage(message); + let mut signature_bytes = [0u8; 64]; + let keydata = self.prv_key_data(wallet_secret).await?; + let private_keys = self.clone().create_address_private_keys(&keydata, &payment_secret, &[address])?; + if private_keys.is_empty() { + return Err(Error::custom(format!("No private key found for address: {}", address))); + } + let private_key = private_keys[0].1; + let public_key = PublicKey::from_secret_key_global(&private_key); + faster_hex::hex_decode(signature.as_bytes(), &mut signature_bytes)?; + Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.into()).is_ok()) + } + async fn get_utxos(self: Arc, addresses: Option>, min_amount_sompi: Option) -> Result> { let utxos = self.utxo_context().get_utxos(addresses, min_amount_sompi).await?; Ok(utxos) diff --git a/wallet/core/src/api/message.rs b/wallet/core/src/api/message.rs index b12a383952..2ccf0c6855 100644 --- a/wallet/core/src/api/message.rs +++ b/wallet/core/src/api/message.rs @@ -647,6 +647,41 @@ impl From for UtxoEntryWrapper { } } +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSignMessageRequest { + pub account_id: AccountId, + pub message: String, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub no_aux_rand: Option, + pub address: Option
, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsSignMessageResponse { + pub signature: String, + pub public_key: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsVerifyMessageRequest { + pub account_id: AccountId, + pub message: String, + pub signature: String, + pub wallet_secret: Secret, + pub payment_secret: Option, + pub address: Option
, +} + +#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[serde(rename_all = "camelCase")] +pub struct AccountsVerifyMessageResponse { + pub verified: bool, +} + #[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[serde(rename_all = "camelCase")] pub struct AccountsTransferRequest { diff --git a/wallet/core/src/api/traits.rs b/wallet/core/src/api/traits.rs index 47ac1e0c5f..6c9f50e4cb 100644 --- a/wallet/core/src/api/traits.rs +++ b/wallet/core/src/api/traits.rs @@ -450,6 +450,25 @@ pub trait WalletApi: Send + Sync + AnySync { /// Get UTXOs for an account. async fn accounts_get_utxos_call(self: Arc, request: AccountsGetUtxosRequest) -> Result; + /// Wrapper around [`accounts_sign_message_call()`](Self::accounts_sign_message_call) + async fn accounts_sign_message(self: Arc, request: AccountsSignMessageRequest) -> Result { + self.accounts_sign_message_call(request).await + } + + /// Sign a message. + async fn accounts_sign_message_call(self: Arc, request: AccountsSignMessageRequest) -> Result; + + /// Wrapper around [`accounts_verify_message_call()`](Self::accounts_verify_message_call) + async fn accounts_verify_message(self: Arc, request: AccountsVerifyMessageRequest) -> Result { + self.accounts_verify_message_call(request).await + } + + /// Verify a message. + async fn accounts_verify_message_call( + self: Arc, + request: AccountsVerifyMessageRequest, + ) -> Result; + /// Transfer funds to another account. Returns an [`AccountsTransferResponse`] /// struct that contains a [`GeneratorSummary`] as well `transaction_ids` /// containing a list of submitted transaction ids. Unlike funds sent to an diff --git a/wallet/core/src/api/transport.rs b/wallet/core/src/api/transport.rs index f3fda45eff..87cae3a6ca 100644 --- a/wallet/core/src/api/transport.rs +++ b/wallet/core/src/api/transport.rs @@ -102,6 +102,8 @@ impl WalletApi for WalletClient { AccountsPskbSign, AccountsPskbBroadcast, AccountsPskbSend, + AccountsSignMessage, + AccountsVerifyMessage, AccountsGetUtxos, AccountsTransfer, AccountsEstimate, @@ -188,6 +190,8 @@ impl WalletServer { AccountsPskbSign, AccountsPskbBroadcast, AccountsPskbSend, + AccountsSignMessage, + AccountsVerifyMessage, AccountsGetUtxos, AccountsTransfer, AccountsEstimate, diff --git a/wallet/core/src/tx/generator/signer.rs b/wallet/core/src/tx/generator/signer.rs index e24ef653d5..32460a5f90 100644 --- a/wallet/core/src/tx/generator/signer.rs +++ b/wallet/core/src/tx/generator/signer.rs @@ -3,8 +3,10 @@ //! use crate::imports::*; +use crate::message::{sign_message, PersonalMessage, SignMessageOptions}; use kaspa_bip32::PrivateKey; use kaspa_consensus_core::{sign::sign_with_multiple_v2, tx::SignableTransaction}; +use secp256k1::PublicKey; pub trait SignerT: Send + Sync + 'static { fn try_sign(&self, transaction: SignableTransaction, addresses: &[Address]) -> Result; @@ -46,6 +48,24 @@ impl Signer { Ok(()) } + + pub async fn sign_message( + &self, + message: &str, + address: &Address, + sign_options: SignMessageOptions, + ) -> Result<(String, PublicKey)> { + let private_keys = + self.inner.account.clone().create_address_private_keys(&self.inner.keydata, &self.inner.payment_secret, &[address])?; + if private_keys.is_empty() { + return Err(Error::custom(format!("No private key found for address: {}", address))); + } + let private_key = private_keys[0].1; + let privkey_bytes = private_key.to_bytes(); + let pm = PersonalMessage(message); + let sig_vec = sign_message(&pm, &privkey_bytes, &sign_options)?; + Ok((faster_hex::hex_string(sig_vec.as_slice()), PublicKey::from_secret_key_global(&private_key))) + } } impl SignerT for Signer { diff --git a/wallet/core/src/wallet/api.rs b/wallet/core/src/wallet/api.rs index 409ce1b04e..fb3cd19691 100644 --- a/wallet/core/src/wallet/api.rs +++ b/wallet/core/src/wallet/api.rs @@ -432,6 +432,34 @@ impl WalletApi for super::Wallet { Ok(AccountsPskbBroadcastResponse { transaction_ids }) } + async fn accounts_sign_message_call(self: Arc, request: AccountsSignMessageRequest) -> Result { + let AccountsSignMessageRequest { account_id, message, wallet_secret, payment_secret, no_aux_rand, address } = request; + + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let address = address.unwrap_or(account.receive_address()?); + let (signature, public_key) = + account.sign_message(&message, &address, wallet_secret, payment_secret, no_aux_rand.unwrap_or(false)).await?; + Ok(AccountsSignMessageResponse { signature, public_key: public_key.to_string() }) + } + + async fn accounts_verify_message_call( + self: Arc, + request: AccountsVerifyMessageRequest, + ) -> Result { + let AccountsVerifyMessageRequest { account_id, message, signature, wallet_secret, payment_secret, address } = request; + + let guard = self.guard(); + let guard = guard.lock().await; + + let account = self.get_account_by_id(&account_id, &guard).await?.ok_or(Error::AccountNotFound(account_id))?; + let address = address.unwrap_or(account.receive_address()?); + let verified = account.verify_message(&message, &signature, &address, wallet_secret, payment_secret).await?; + Ok(AccountsVerifyMessageResponse { verified }) + } + async fn accounts_get_utxos_call(self: Arc, request: AccountsGetUtxosRequest) -> Result { let AccountsGetUtxosRequest { account_id, addresses, min_amount_sompi } = request; let guard = self.guard(); diff --git a/wallet/core/src/wasm/api/message.rs b/wallet/core/src/wasm/api/message.rs index 2728227311..25ea4a9bc2 100644 --- a/wallet/core/src/wasm/api/message.rs +++ b/wallet/core/src/wasm/api/message.rs @@ -1647,6 +1647,116 @@ try_from! ( args: AccountsPskbSendResponse, IAccountsPskbSendResponse, { // --- +declare! { + IAccountsSignMessageRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsSignMessageRequest { + accountId : HexString; + message : string; + walletSecret : string; + paymentSecret? : string; + noAuxRand? : boolean; + address? : Address | string; + } + "#, +} + +try_from! ( args: IAccountsSignMessageRequest, AccountsSignMessageRequest, { + let account_id = args.get_account_id("accountId")?; + let message = args.get_string("message")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let no_aux_rand = args.get_bool("noAuxRand").ok(); + let address = match args.try_get_value("address")? { + Some(v) => Some(Address::try_cast_from(&v)?.into_owned()), + None => None, + }; + Ok(AccountsSignMessageRequest { account_id, message, wallet_secret, payment_secret, no_aux_rand, address }) +}); + +declare! { + IAccountsSignMessageResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsSignMessageResponse { + signature : string; + publicKey : string; + } + "#, +} + +try_from! ( args: AccountsSignMessageResponse, IAccountsSignMessageResponse, { + let response = IAccountsSignMessageResponse::default(); + response.set("signature", &args.signature.into())?; + response.set("publicKey", &args.public_key.into())?; + Ok(response) +}); + +// --- + +declare! { + IAccountsVerifyMessageRequest, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsVerifyMessageRequest { + accountId : HexString; + message : string; + signature : string; + walletSecret : string; + paymentSecret? : string; + address? : Address | string; + } + "#, +} + +try_from! ( args: IAccountsVerifyMessageRequest, AccountsVerifyMessageRequest, { + let account_id = args.get_account_id("accountId")?; + let message = args.get_string("message")?; + let signature = args.get_string("signature")?; + let wallet_secret = args.get_secret("walletSecret")?; + let payment_secret = args.try_get_secret("paymentSecret")?; + let address = match args.try_get_value("address")? { + Some(v) => Some(Address::try_cast_from(&v)?.into_owned()), + None => None, + }; + Ok(AccountsVerifyMessageRequest { account_id, message, signature, wallet_secret, payment_secret, address }) +}); + +declare! { + IAccountsVerifyMessageResponse, + r#" + /** + * + * + * @category Wallet API + */ + export interface IAccountsVerifyMessageResponse { + verified : boolean; + } + "#, +} + +try_from! ( args: AccountsVerifyMessageResponse, IAccountsVerifyMessageResponse, { + let response = IAccountsVerifyMessageResponse::default(); + response.set("verified", &args.verified.into())?; + Ok(response) +}); + +// --- + declare! { IAccountsGetUtxosRequest, r#" diff --git a/wallet/core/src/wasm/api/mod.rs b/wallet/core/src/wasm/api/mod.rs index 2f34d62fb0..6c443a57fd 100644 --- a/wallet/core/src/wasm/api/mod.rs +++ b/wallet/core/src/wasm/api/mod.rs @@ -49,6 +49,8 @@ declare_wasm_handlers!([ AccountsPskbSign, AccountsPskbBroadcast, AccountsPskbSend, + AccountsSignMessage, + AccountsVerifyMessage, AccountsGetUtxos, AccountsTransfer, AccountsEstimate, diff --git a/wasm/examples/nodejs/javascript/wallet/message-signing.js b/wasm/examples/nodejs/javascript/wallet/message-signing.js new file mode 100644 index 0000000000..75d899d02f --- /dev/null +++ b/wasm/examples/nodejs/javascript/wallet/message-signing.js @@ -0,0 +1,133 @@ +// @ts-ignore +globalThis.WebSocket = require('websocket').w3cwebsocket; // W3C WebSocket module shim + + +const path = require('path'); +const fs = require('fs'); +const kaspa = require('../../../../nodejs/kaspa-dev'); +const { + Wallet, setDefaultStorageFolder, + AccountKind, Resolver, + sompiToKaspaString, + Address, + verifyMessage +} = kaspa; + +let storageFolder = path.join(__dirname, '../../../data/wallets').normalize(); +if (!fs.existsSync(storageFolder)) { + fs.mkdirSync(storageFolder); +} + +setDefaultStorageFolder(storageFolder); + +(async()=>{ + //const filename = "wallet-394"; + const filename = "wallet-395"; + + const balance = {}; + let wallet; + + const chalk = new ((await import('chalk')).Chalk)(); + + function log_title(title){ + console.log(chalk.bold(chalk.green(`\n\n${title}`))) + } + + try { + + const walletSecret = "abc"; + wallet = new Wallet({resident: false, networkId: "testnet-11", resolver: new Resolver()}); + //console.log("wallet", wallet) + // Ensure wallet file + if (!await wallet.exists(filename)){ + let response = await wallet.walletCreate({ + walletSecret, + filename, + title: "W-1" + }); + console.log("walletCreate : response", response) + } + + // Open wallet + await wallet.walletOpen({ + walletSecret, + filename, + accountDescriptors: false + }); + + // Ensure default account + await wallet.accountsEnsureDefault({ + walletSecret, + type: new AccountKind("bip32") // "bip32" + }); + + // Connect to rpc + await wallet.connect(); + + // Start wallet processing + await wallet.start(); + let accounts; + let firstAccount = {}; + async function listAccount(){ + // List accounts + accounts = await wallet.accountsEnumerate({}); + firstAccount = accounts.accountDescriptors[0]; + + //console.log("firstAccount:", firstAccount); + + // Activate Account + await wallet.accountsActivate({ + accountIds:[firstAccount.accountId] + }); + + // log_title("Accounts"); + // accounts.accountDescriptors.forEach(a=>{ + // console.log(`Account: ${a.accountId}`); + // console.log(` Account type: ${a.kind.toString()}`); + // console.log(` Account Name: ${a.accountName}`); + // console.log(` Receive Address: ${a.receiveAddress}`); + // console.log(` Change Address: ${a.changeAddress}`); + // console.log("") + // }); + } + + await listAccount(); + + log_title("Message Signing"); + + const message = "Hello Kaspa!"; + const signResponse = await wallet.accountsSignMessage({ + accountId: firstAccount.accountId, + message, + walletSecret, + //optional noAuxRand, if not provided, it will use the auxiliary randomness + //please see: + //noAuxRand: true, + + //optional address, default is receive address + //address: firstAccount.changeAddress + }); + + console.log("Sign response:", signResponse); + + const verificationResponse = await wallet.accountsVerifyMessage({ + accountId: firstAccount.accountId, + message, + signature: signResponse.signature, + walletSecret, + //optional address, default is receive address + //address: firstAccount.changeAddress + }); + + console.log("Verification response:", verificationResponse); + + // if (verifyMessage({message, signature: signResponse.signature, publicKey: signResponse.publicKey})) { + // console.info('Signature verified!'); + // } else { + // console.info('Signature is invalid!'); + // } + + } catch(ex) { + console.error("Error:", ex); + } +})();