From 5e43f469e7a3ea80fa0fce98665dbc9cbd688b54 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Thu, 2 Jan 2025 20:33:29 -0500 Subject: [PATCH 1/9] Initial lift --- crates/sage-database/src/derivations.rs | 43 ++++++++++++++------- crates/sage-wallet/src/wallet/signing.rs | 21 ++++++---- crates/sage/src/endpoints/data.rs | 2 +- crates/sage/src/endpoints/wallet_connect.rs | 11 ++++-- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/crates/sage-database/src/derivations.rs b/crates/sage-database/src/derivations.rs index ace56ecc..ad60ea2b 100644 --- a/crates/sage-database/src/derivations.rs +++ b/crates/sage-database/src/derivations.rs @@ -5,13 +5,20 @@ use crate::{ into_row, to_bytes, to_bytes32, Database, DatabaseTx, DerivationRow, DerivationSql, Result, }; +#[derive(Debug, Clone, Copy)] +pub struct SyntheticKeyInfo { + pub index: u32, + pub hardened: bool, +} + impl Database { - pub async fn unhardened_derivations( + pub async fn derivations( &self, + hardened: bool, limit: u32, offset: u32, ) -> Result> { - unhardened_derivations(&self.pool, limit, offset).await + derivations(&self.pool, hardened, limit, offset).await } pub async fn p2_puzzle_hashes(&self) -> Result> { @@ -22,8 +29,11 @@ impl Database { synthetic_key(&self.pool, p2_puzzle_hash).await } - pub async fn synthetic_key_index(&self, synthetic_key: PublicKey) -> Result> { - synthetic_key_index(&self.pool, synthetic_key).await + pub async fn synthetic_key_info( + &self, + synthetic_key: PublicKey, + ) -> Result> { + synthetic_key_info(&self.pool, synthetic_key).await } pub async fn is_p2_puzzle_hash(&self, p2_puzzle_hash: Bytes32) -> Result { @@ -148,8 +158,9 @@ async fn p2_puzzle_hashes(conn: impl SqliteExecutor<'_>) -> Result> .collect::>() } -async fn unhardened_derivations( +async fn derivations( conn: impl SqliteExecutor<'_>, + hardened: bool, limit: u32, offset: u32, ) -> Result> { @@ -157,10 +168,11 @@ async fn unhardened_derivations( DerivationSql, " SELECT * FROM `derivations` - WHERE `hardened` = 0 + WHERE `hardened` = ? ORDER BY `index` ASC LIMIT ? OFFSET ? ", + hardened, limit, offset ) @@ -190,25 +202,30 @@ async fn synthetic_key( Ok(PublicKey::from_bytes(&to_bytes(bytes)?)?) } -async fn synthetic_key_index( +async fn synthetic_key_info( conn: impl SqliteExecutor<'_>, synthetic_key: PublicKey, -) -> Result> { +) -> Result> { let synthetic_key = synthetic_key.to_bytes(); let synthetic_key_ref = synthetic_key.as_ref(); - Ok(sqlx::query!( + + sqlx::query!( " - SELECT `index` + SELECT `index`, `hardened` FROM `derivations` WHERE `synthetic_key` = ? - AND `hardened` = 0 ", synthetic_key_ref ) .fetch_optional(conn) .await? - .map(|row| row.index.try_into()) - .transpose()?) + .map(|row| { + Ok(SyntheticKeyInfo { + index: row.index.try_into()?, + hardened: row.hardened, + }) + }) + .transpose() } async fn p2_puzzle_hash( diff --git a/crates/sage-wallet/src/wallet/signing.rs b/crates/sage-wallet/src/wallet/signing.rs index 829c3cd4..2046e1f6 100644 --- a/crates/sage-wallet/src/wallet/signing.rs +++ b/crates/sage-wallet/src/wallet/signing.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use chia::{ bls::{ - master_to_wallet_unhardened_intermediate, sign, DerivableKey, PublicKey, SecretKey, - Signature, + master_to_wallet_hardened_intermediate, master_to_wallet_unhardened_intermediate, sign, + DerivableKey, PublicKey, SecretKey, Signature, }, protocol::{CoinSpend, SpendBundle}, puzzles::DeriveSynthetic, @@ -73,21 +73,28 @@ impl Wallet { return Err(WalletError::SecpNotSupported); }; let pk = required.public_key; - let Some(index) = self.db.synthetic_key_index(pk).await? else { + let Some(info) = self.db.synthetic_key_info(pk).await? else { if partial { continue; } return Err(WalletError::UnknownPublicKey); }; - indices.insert(pk, index); + indices.insert(pk, info); } - let intermediate_sk = master_to_wallet_unhardened_intermediate(&master_sk); + let unhardened_intermediate_sk = master_to_wallet_unhardened_intermediate(&master_sk); + let hardened_intermediate_sk = master_to_wallet_hardened_intermediate(&master_sk); let secret_keys: HashMap = indices .iter() - .map(|(pk, index)| { - let sk = intermediate_sk.derive_unhardened(*index).derive_synthetic(); + .map(|(pk, info)| { + let sk = if info.hardened { + hardened_intermediate_sk.derive_hardened(info.index) + } else { + unhardened_intermediate_sk.derive_unhardened(info.index) + } + .derive_synthetic(); + (*pk, sk) }) .collect(); diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index 854f7196..47486066 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -64,7 +64,7 @@ impl Sage { let derivations = wallet .db - .unhardened_derivations(req.limit, req.offset) + .derivations(false, req.limit, req.offset) .await? .into_iter() .map(|row| { diff --git a/crates/sage/src/endpoints/wallet_connect.rs b/crates/sage/src/endpoints/wallet_connect.rs index 5fbcb08f..b28225e0 100644 --- a/crates/sage/src/endpoints/wallet_connect.rs +++ b/crates/sage/src/endpoints/wallet_connect.rs @@ -1,5 +1,5 @@ use chia::{ - bls::{master_to_wallet_unhardened, sign}, + bls::{master_to_wallet_hardened, master_to_wallet_unhardened, sign}, clvm_utils::ToTreeHash, protocol::{Bytes, Coin, CoinSpend, SpendBundle}, puzzles::{cat::CatArgs, standard::StandardArgs, DeriveSynthetic, Proof}, @@ -330,7 +330,7 @@ impl Sage { let wallet = self.wallet()?; let public_key = parse_public_key(req.public_key)?; - let Some(index) = wallet.db.synthetic_key_index(public_key).await? else { + let Some(info) = wallet.db.synthetic_key_info(public_key).await? else { return Err(Error::InvalidKey); }; @@ -340,7 +340,12 @@ impl Sage { return Err(Error::NoSigningKey); }; - let secret_key = master_to_wallet_unhardened(&master_sk, index).derive_synthetic(); + let secret_key = if info.hardened { + master_to_wallet_hardened(&master_sk, info.index) + } else { + master_to_wallet_unhardened(&master_sk, info.index) + } + .derive_synthetic(); let decoded_message = Bytes::from(hex::decode(&req.message)?); let signature = sign( From 2230c1a898d27a0e4d0b0d78bd203817d9ff34fc Mon Sep 17 00:00:00 2001 From: Rigidity Date: Thu, 2 Jan 2025 20:36:38 -0500 Subject: [PATCH 2/9] Prepare --- ...861d96ec5e166b3375a365f6938a43aff5c11.json | 26 +++++++++++++++++++ ...7ea986ab761fe442df4e343fe8dc55ac7ad2.json} | 6 ++--- ...0af9fa06480ce6c0b2821713c762c02b54af9.json | 20 -------------- 3 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 .sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json rename .sqlx/{query-8bc14ce79ff1a599fa6cb5bda7f22d2f31b05b3980015fd6f885ba585af8524e.json => query-9a831561a6da3332d62751a0e0657ea986ab761fe442df4e343fe8dc55ac7ad2.json} (80%) delete mode 100644 .sqlx/query-cf4af3005a8ec67a1245a2f789f0af9fa06480ce6c0b2821713c762c02b54af9.json diff --git a/.sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json b/.sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json new file mode 100644 index 00000000..b71da9b2 --- /dev/null +++ b/.sqlx/query-72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT `index`, `hardened`\n FROM `derivations`\n WHERE `synthetic_key` = ?\n ", + "describe": { + "columns": [ + { + "name": "index", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "hardened", + "ordinal": 1, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "72c9608d6e6a3deefc13e74e367861d96ec5e166b3375a365f6938a43aff5c11" +} diff --git a/.sqlx/query-8bc14ce79ff1a599fa6cb5bda7f22d2f31b05b3980015fd6f885ba585af8524e.json b/.sqlx/query-9a831561a6da3332d62751a0e0657ea986ab761fe442df4e343fe8dc55ac7ad2.json similarity index 80% rename from .sqlx/query-8bc14ce79ff1a599fa6cb5bda7f22d2f31b05b3980015fd6f885ba585af8524e.json rename to .sqlx/query-9a831561a6da3332d62751a0e0657ea986ab761fe442df4e343fe8dc55ac7ad2.json index 3fa6419f..89cbf882 100644 --- a/.sqlx/query-8bc14ce79ff1a599fa6cb5bda7f22d2f31b05b3980015fd6f885ba585af8524e.json +++ b/.sqlx/query-9a831561a6da3332d62751a0e0657ea986ab761fe442df4e343fe8dc55ac7ad2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT * FROM `derivations`\n WHERE `hardened` = 0\n ORDER BY `index` ASC\n LIMIT ? OFFSET ?\n ", + "query": "\n SELECT * FROM `derivations`\n WHERE `hardened` = ?\n ORDER BY `index` ASC\n LIMIT ? OFFSET ?\n ", "describe": { "columns": [ { @@ -25,7 +25,7 @@ } ], "parameters": { - "Right": 2 + "Right": 3 }, "nullable": [ false, @@ -34,5 +34,5 @@ false ] }, - "hash": "8bc14ce79ff1a599fa6cb5bda7f22d2f31b05b3980015fd6f885ba585af8524e" + "hash": "9a831561a6da3332d62751a0e0657ea986ab761fe442df4e343fe8dc55ac7ad2" } diff --git a/.sqlx/query-cf4af3005a8ec67a1245a2f789f0af9fa06480ce6c0b2821713c762c02b54af9.json b/.sqlx/query-cf4af3005a8ec67a1245a2f789f0af9fa06480ce6c0b2821713c762c02b54af9.json deleted file mode 100644 index f4f54c6d..00000000 --- a/.sqlx/query-cf4af3005a8ec67a1245a2f789f0af9fa06480ce6c0b2821713c762c02b54af9.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n SELECT `index`\n FROM `derivations`\n WHERE `synthetic_key` = ?\n AND `hardened` = 0\n ", - "describe": { - "columns": [ - { - "name": "index", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "cf4af3005a8ec67a1245a2f789f0af9fa06480ce6c0b2821713c762c02b54af9" -} From 9610db9184d9a3e9a23de99ca683491d44494cd7 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Thu, 2 Jan 2025 22:18:55 -0500 Subject: [PATCH 3/9] Generate hardened keys on import --- .../src/sync_manager/wallet_sync.rs | 110 ++++++++---------- crates/sage-wallet/src/test.rs | 29 ++++- crates/sage-wallet/src/wallet/p2_send.rs | 39 +++++++ crates/sage/src/endpoints/keys.rs | 69 +++++++++-- 4 files changed, 171 insertions(+), 76 deletions(-) diff --git a/crates/sage-wallet/src/sync_manager/wallet_sync.rs b/crates/sage-wallet/src/sync_manager/wallet_sync.rs index 3f7f9802..ef91feb8 100644 --- a/crates/sage-wallet/src/sync_manager/wallet_sync.rs +++ b/crates/sage-wallet/src/sync_manager/wallet_sync.rs @@ -3,15 +3,10 @@ use std::{ time::{Duration, Instant}, }; -use chia::{ - bls::DerivableKey, - protocol::{Bytes32, CoinState, CoinStateFilters}, - puzzles::{standard::StandardArgs, DeriveSynthetic}, -}; -use rayon::iter::{IntoParallelIterator, ParallelIterator}; +use chia::protocol::{Bytes32, CoinState, CoinStateFilters}; +use sage_database::DatabaseTx; use tokio::{ sync::{mpsc, Mutex}, - task::spawn_blocking, time::{sleep, timeout}, }; use tracing::{debug, info, warn}; @@ -50,10 +45,8 @@ pub async fn sync_wallet( ) .await?; - let mut derive_more = p2_puzzle_hashes.is_empty(); - for batch in p2_puzzle_hashes.chunks(500) { - derive_more |= sync_puzzle_hashes( + sync_puzzle_hashes( &wallet, &peer, start_height, @@ -64,49 +57,25 @@ pub async fn sync_wallet( .await?; } - let mut start_index = p2_puzzle_hashes.len() as u32; - - while derive_more { - derive_more = false; - - let intermediate_pk = wallet.intermediate_pk; - - let new_derivations = spawn_blocking(move || { - (start_index..start_index + 500) - .into_par_iter() - .map(|index| { - let synthetic_key = intermediate_pk.derive_unhardened(index).derive_synthetic(); - let p2_puzzle_hash = - Bytes32::from(StandardArgs::curry_tree_hash(synthetic_key)); - (index, synthetic_key, p2_puzzle_hash) - }) - .collect::>() - }) - .await?; - - let p2_puzzle_hashes: Vec = new_derivations - .iter() - .map(|(_, _, p2_puzzle_hash)| *p2_puzzle_hash) - .collect(); - - start_index += new_derivations.len() as u32; - + loop { let mut tx = wallet.db.tx().await?; - for (index, synthetic_key, p2_puzzle_hash) in new_derivations { - tx.insert_derivation(p2_puzzle_hash, index, false, synthetic_key) - .await?; - } + let derivations = auto_insert_unhardened_derivations(&wallet, &mut tx).await?; + let next_index = tx.derivation_index(false).await?; tx.commit().await?; + if derivations.is_empty() { + break; + } + + info!("Inserted {} derivations", derivations.len()); + sync_sender - .send(SyncEvent::DerivationIndex { - next_index: start_index, - }) + .send(SyncEvent::DerivationIndex { next_index }) .await .ok(); - for batch in p2_puzzle_hashes.chunks(500) { - derive_more |= sync_puzzle_hashes( + for batch in derivations.chunks(500) { + sync_puzzle_hashes( &wallet, &peer, None, @@ -174,10 +143,9 @@ async fn sync_puzzle_hashes( start_header_hash: Bytes32, puzzle_hashes: &[Bytes32], sync_sender: mpsc::Sender, -) -> Result { +) -> Result<(), WalletError> { let mut prev_height = start_height; let mut prev_header_hash = start_header_hash; - let mut found_coins = false; loop { debug!( @@ -201,7 +169,6 @@ async fn sync_puzzle_hashes( debug!("Received {} coin states", data.coin_states.len()); if !data.coin_states.is_empty() { - found_coins = true; incremental_sync(wallet, data.coin_states, true, &sync_sender).await?; } @@ -213,7 +180,7 @@ async fn sync_puzzle_hashes( } } - Ok(found_coins) + Ok(()) } pub async fn incremental_sync( @@ -247,24 +214,14 @@ pub async fn incremental_sync( let mut derived = false; - let mut next_index = tx.derivation_index(false).await?; - if derive_automatically { - let max_index = tx - .max_used_derivation_index(false) + derived = !auto_insert_unhardened_derivations(wallet, &mut tx) .await? - .map_or(0, |index| index + 1); - - while next_index < max_index + 500 { - wallet - .insert_unhardened_derivations(&mut tx, next_index..next_index + 500) - .await?; - - derived = true; - next_index += 500; - } + .is_empty(); } + let next_index = tx.derivation_index(false).await?; + tx.commit().await?; sync_sender.send(SyncEvent::CoinsUpdated).await.ok(); @@ -278,3 +235,28 @@ pub async fn incremental_sync( Ok(()) } + +async fn auto_insert_unhardened_derivations( + wallet: &Wallet, + tx: &mut DatabaseTx<'_>, +) -> Result, WalletError> { + let mut derivations = Vec::new(); + let mut next_index = tx.derivation_index(false).await?; + + let max_index = tx + .max_used_derivation_index(false) + .await? + .map_or(0, |index| index + 1); + + while next_index < max_index + 500 { + derivations.extend( + wallet + .insert_unhardened_derivations(tx, next_index..next_index + 500) + .await?, + ); + + next_index += 500; + } + + Ok(derivations) +} diff --git a/crates/sage-wallet/src/test.rs b/crates/sage-wallet/src/test.rs index de6dac18..ad12c865 100644 --- a/crates/sage-wallet/src/test.rs +++ b/crates/sage-wallet/src/test.rs @@ -1,7 +1,10 @@ use std::{sync::Arc, time::Duration}; use chia::{ - bls::{master_to_wallet_unhardened_intermediate, DerivableKey, SecretKey}, + bls::{ + master_to_wallet_hardened, master_to_wallet_hardened_intermediate, + master_to_wallet_unhardened_intermediate, DerivableKey, SecretKey, + }, protocol::{Bytes32, CoinSpend, SpendBundle}, puzzles::{standard::StandardArgs, DeriveSynthetic}, }; @@ -34,6 +37,7 @@ pub struct TestWallet { pub wallet: Arc, pub master_sk: SecretKey, pub puzzle_hash: Bytes32, + pub hardened_puzzle_hash: Bytes32, pub sender: Sender, pub events: Receiver, pub index: u32, @@ -72,9 +76,31 @@ impl TestWallet { let intermediate_pk = master_to_wallet_unhardened_intermediate(&pk); let genesis_challenge = TESTNET11_CONSTANTS.genesis_challenge; + let intermediate_hardened_sk = master_to_wallet_hardened_intermediate(&sk); + + let mut tx = db.tx().await?; + + for index in 0..100 { + let synthetic_key = intermediate_hardened_sk + .derive_hardened(index) + .derive_synthetic() + .public_key(); + let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); + tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) + .await?; + } + + tx.commit().await?; + let puzzle_hash = StandardArgs::curry_tree_hash(intermediate_pk.derive_unhardened(0).derive_synthetic()); + let hardened_puzzle_hash = StandardArgs::curry_tree_hash( + master_to_wallet_hardened(&sk, 0) + .derive_synthetic() + .public_key(), + ); + if balance > 0 { sim.mint_coin(puzzle_hash.into(), balance).await; } @@ -129,6 +155,7 @@ impl TestWallet { wallet, master_sk: sk, puzzle_hash: puzzle_hash.into(), + hardened_puzzle_hash: hardened_puzzle_hash.into(), sender, events, index: key_index, diff --git a/crates/sage-wallet/src/wallet/p2_send.rs b/crates/sage-wallet/src/wallet/p2_send.rs index 2ddd7e90..4eb98dce 100644 --- a/crates/sage-wallet/src/wallet/p2_send.rs +++ b/crates/sage-wallet/src/wallet/p2_send.rs @@ -95,4 +95,43 @@ mod tests { Ok(()) } + + #[test(tokio::test)] + async fn test_send_xch_hardened() -> anyhow::Result<()> { + let mut test = TestWallet::new(1000).await?; + + let coin_spends = test + .wallet + .send_xch( + vec![(test.hardened_puzzle_hash, 1000)], + 0, + Vec::new(), + true, + true, + ) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.balance().await?, 1000); + assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); + + let coin_spends = test + .wallet + .send_xch(vec![(test.puzzle_hash, 1000)], 0, Vec::new(), false, true) + .await?; + + assert_eq!(coin_spends.len(), 1); + + test.transact(coin_spends).await?; + test.wait_for_coins().await; + + assert_eq!(test.wallet.db.balance().await?, 1000); + assert_eq!(test.wallet.db.spendable_coins().await?.len(), 1); + + Ok(()) + } } diff --git a/crates/sage/src/endpoints/keys.rs b/crates/sage/src/endpoints/keys.rs index 7e9c4dd0..c43483ad 100644 --- a/crates/sage/src/endpoints/keys.rs +++ b/crates/sage/src/endpoints/keys.rs @@ -1,7 +1,13 @@ use std::{fs, str::FromStr}; use bip39::Mnemonic; -use chia::bls::{PublicKey, SecretKey}; +use chia::{ + bls::{ + master_to_wallet_hardened_intermediate, master_to_wallet_unhardened_intermediate, + DerivableKey, PublicKey, SecretKey, + }, + puzzles::{standard::StandardArgs, DeriveSynthetic}, +}; use itertools::Itertools; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -11,6 +17,8 @@ use sage_api::{ ImportKeyResponse, KeyInfo, KeyKind, Login, LoginResponse, Logout, LogoutResponse, RenameKey, RenameKeyResponse, Resync, ResyncResponse, SecretKeyInfo, }; +use sage_config::WalletConfig; +use sage_database::Database; use crate::{Error, Result, Sage}; @@ -88,30 +96,36 @@ impl Sage { key_hex = &key_hex[2..]; } - let fingerprint = if let Ok(bytes) = hex::decode(key_hex) { + let (fingerprint, master_sk, master_pk) = if let Ok(bytes) = hex::decode(key_hex) { if let Ok(master_pk) = bytes.clone().try_into() { let master_pk = PublicKey::from_bytes(&master_pk)?; - self.keychain.add_public_key(&master_pk)? + let fingerprint = self.keychain.add_public_key(&master_pk)?; + (fingerprint, None, master_pk) } else if let Ok(master_sk) = bytes.try_into() { let master_sk = SecretKey::from_bytes(&master_sk)?; + let master_pk = master_sk.public_key(); - if req.save_secrets { + let fingerprint = if req.save_secrets { self.keychain.add_secret_key(&master_sk, b"")? } else { - self.keychain.add_public_key(&master_sk.public_key())? - } + self.keychain.add_public_key(&master_pk)? + }; + + (fingerprint, Some(master_sk), master_pk) } else { return Err(Error::InvalidKey); } } else { let mnemonic = Mnemonic::from_str(&req.key)?; - - if req.save_secrets { + let master_sk = SecretKey::from_seed(&mnemonic.to_seed("")); + let master_pk = master_sk.public_key(); + let fingerprint = if req.save_secrets { self.keychain.add_mnemonic(&mnemonic, b"")? } else { - let master_sk = SecretKey::from_seed(&mnemonic.to_seed("")); - self.keychain.add_public_key(&master_sk.public_key())? - } + self.keychain.add_public_key(&master_pk)? + }; + + (fingerprint, Some(master_sk), master_pk) }; let config = self.wallet_config_mut(fingerprint); @@ -121,6 +135,39 @@ impl Sage { self.save_keychain()?; self.save_config()?; + let pool = self.connect_to_database(fingerprint).await?; + let db = Database::new(pool); + + let mut tx = db.tx().await?; + + let intermediate_unhardened_pk = master_to_wallet_unhardened_intermediate(&master_pk); + let batch_size = WalletConfig::default().derivation_batch_size; + + for index in 0..batch_size { + let synthetic_key = intermediate_unhardened_pk + .derive_unhardened(index) + .derive_synthetic(); + let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); + tx.insert_derivation(p2_puzzle_hash, index, false, synthetic_key) + .await?; + } + + if let Some(master_sk) = master_sk { + let intermediate_hardened_sk = master_to_wallet_hardened_intermediate(&master_sk); + + for index in 0..batch_size { + let synthetic_key = intermediate_hardened_sk + .derive_hardened(index) + .derive_synthetic() + .public_key(); + let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); + tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) + .await?; + } + } + + tx.commit().await?; + if req.login { self.switch_wallet().await?; } From 20c3c3a085d3d9e04a355e1da85be7abc88ff7ce Mon Sep 17 00:00:00 2001 From: Rigidity Date: Thu, 2 Jan 2025 23:03:56 -0500 Subject: [PATCH 4/9] Initial hardened key UI --- crates/sage-api/src/requests/data.rs | 2 ++ crates/sage/src/endpoints/data.rs | 2 +- src/bindings.ts | 2 +- src/components/AddressList.tsx | 12 +++++------ src/pages/Addresses.tsx | 31 ++++++++++++++++++++++++---- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index 62d7f008..ef34b502 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -8,6 +8,8 @@ use crate::{ #[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] pub struct GetDerivations { + #[serde(default)] + pub hardened: bool, pub offset: u32, pub limit: u32, } diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index 47486066..e355e058 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -64,7 +64,7 @@ impl Sage { let derivations = wallet .db - .derivations(false, req.limit, req.offset) + .derivations(req.hardened, req.limit, req.offset) .await? .into_iter() .map(|row| { diff --git a/src/bindings.ts b/src/bindings.ts index 2d100bd0..1719994e 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -282,7 +282,7 @@ export type GetCatCoinsResponse = { coins: CoinRecord[] } export type GetCatResponse = { cat: CatRecord | null } export type GetCats = Record export type GetCatsResponse = { cats: CatRecord[] } -export type GetDerivations = { offset: number; limit: number } +export type GetDerivations = { hardened?: boolean; offset: number; limit: number } export type GetDerivationsResponse = { derivations: DerivationRecord[] } export type GetDids = Record export type GetDidsResponse = { dids: DidRecord[] } diff --git a/src/components/AddressList.tsx b/src/components/AddressList.tsx index 18a25f31..b544348f 100644 --- a/src/components/AddressList.tsx +++ b/src/components/AddressList.tsx @@ -6,6 +6,8 @@ import { TableHeader, TableRow, } from '@/components/ui/table'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; import { ColumnDef, flexRender, @@ -15,11 +17,9 @@ import { } from '@tanstack/react-table'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { useMemo } from 'react'; -import { Trans } from '@lingui/react/macro'; -import { t } from '@lingui/core/macro'; -import { Button } from './ui/button'; import { CopyButton } from './CopyButton'; import { FormattedAddress } from './FormattedAddress'; +import { Button } from './ui/button'; export interface AddressListProps { addresses: string[]; @@ -94,15 +94,15 @@ export default function AddressList(props: AddressListProps) { columnResizeMode: 'onChange', initialState: { pagination: { - pageSize: 10, + pageSize: 100, }, }, }); return ( -
+
-
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/pages/Addresses.tsx b/src/pages/Addresses.tsx index 7ba437e9..9a9b1baa 100644 --- a/src/pages/Addresses.tsx +++ b/src/pages/Addresses.tsx @@ -1,6 +1,7 @@ import Container from '@/components/Container'; import Header from '@/components/Header'; import { ReceiveAddress } from '@/components/ReceiveAddress'; +import { Button } from '@/components/ui/button'; import { Card, CardContent, @@ -8,29 +9,31 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; import { useErrors } from '@/hooks/useErrors'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; import { useCallback, useEffect, useState } from 'react'; import { commands, events } from '../bindings'; import AddressList from '../components/AddressList'; import { useWalletState } from '../state'; -import { Trans } from '@lingui/react/macro'; -import { t } from '@lingui/core/macro'; export default function Addresses() { const { addError } = useErrors(); const walletState = useWalletState(); const ticker = walletState.sync.unit.ticker; + const [hardened, setHardened] = useState(false); const [addresses, setAddresses] = useState([]); const updateAddresses = useCallback(() => { commands - .getDerivations({ offset: 0, limit: 1000000 }) + .getDerivations({ offset: 0, limit: 1000000, hardened }) .then((data) => setAddresses(data.derivations.map((derivation) => derivation.address)), ) .catch(addError); - }, [addError]); + }, [addError, hardened]); useEffect(() => { updateAddresses(); @@ -46,6 +49,8 @@ export default function Addresses() { }; }, [updateAddresses]); + const derivationIndex = addresses.length; + return ( <>
@@ -74,6 +79,24 @@ export default function Addresses() { +
+ + setHardened(value)} + /> +
+ +
+ The current derivation index is {derivationIndex} + +
+
From 656a4579fc59ce6f4f06bf83864d0ddc49068dcb Mon Sep 17 00:00:00 2001 From: Rigidity Date: Fri, 3 Jan 2025 09:57:37 -0500 Subject: [PATCH 5/9] Get hardened keys working --- ...3803d8c3e38cb0101e01af1ed59bd7f64e078.json | 12 ++ ...c03ce433aa3e425e0ca136766591d10497281.json | 12 ++ crates/sage-api/src/requests/actions.rs | 9 ++ crates/sage-api/src/requests/keys.rs | 4 + crates/sage-cli/src/router.rs | 1 + crates/sage-wallet/src/sync_manager.rs | 54 ++++++-- .../src/sync_manager/sync_command.rs | 3 + crates/sage/src/endpoints/actions.rs | 69 +++++++++- crates/sage/src/endpoints/keys.rs | 12 ++ src-tauri/src/commands.rs | 9 ++ src-tauri/src/lib.rs | 1 + src/bindings.ts | 7 +- src/pages/Addresses.tsx | 130 +++++++++++++++++- src/pages/Login.tsx | 28 +++- 14 files changed, 330 insertions(+), 21 deletions(-) create mode 100644 .sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json create mode 100644 .sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json diff --git a/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json b/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json new file mode 100644 index 00000000..52d7f7ae --- /dev/null +++ b/.sqlx/query-0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM `derivations` WHERE `hardened` = 0", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "0e221ba6a44bdd687317f09e1003803d8c3e38cb0101e01af1ed59bd7f64e078" +} diff --git a/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json b/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json new file mode 100644 index 00000000..5ff6b4b6 --- /dev/null +++ b/.sqlx/query-211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM `derivations` WHERE `hardened` = 1", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "211edf13476a2c9e6bfb5d3b026c03ce433aa3e425e0ca136766591d10497281" +} diff --git a/crates/sage-api/src/requests/actions.rs b/crates/sage-api/src/requests/actions.rs index 19d3731a..0aee780a 100644 --- a/crates/sage-api/src/requests/actions.rs +++ b/crates/sage-api/src/requests/actions.rs @@ -37,3 +37,12 @@ pub struct UpdateNft { #[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] pub struct UpdateNftResponse {} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] +pub struct IncreaseDerivationIndex { + pub hardened: bool, + pub index: u32, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] +pub struct IncreaseDerivationIndexResponse {} diff --git a/crates/sage-api/src/requests/keys.rs b/crates/sage-api/src/requests/keys.rs index f4cdc01a..416b0667 100644 --- a/crates/sage-api/src/requests/keys.rs +++ b/crates/sage-api/src/requests/keys.rs @@ -22,6 +22,10 @@ pub struct Resync { pub fingerprint: u32, #[serde(default)] pub delete_offer_files: bool, + #[serde(default)] + pub delete_unhardened_derivations: bool, + #[serde(default)] + pub delete_hardened_derivations: bool, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, Type)] diff --git a/crates/sage-cli/src/router.rs b/crates/sage-cli/src/router.rs index f9ddb40f..e5064a26 100644 --- a/crates/sage-cli/src/router.rs +++ b/crates/sage-cli/src/router.rs @@ -130,6 +130,7 @@ routes!( update_cat await: UpdateCat = "/update_cat", update_did await: UpdateDid = "/update_did", update_nft await: UpdateNft = "/update_nft", + increase_derivation_index await: IncreaseDerivationIndex = "/increase_derivation_index", ); async fn start_rpc(path: PathBuf) -> Result<()> { diff --git a/crates/sage-wallet/src/sync_manager.rs b/crates/sage-wallet/src/sync_manager.rs index 9ea46fab..812230f6 100644 --- a/crates/sage-wallet/src/sync_manager.rs +++ b/crates/sage-wallet/src/sync_manager.rs @@ -6,7 +6,9 @@ use std::{ }; use chia::{ - protocol::{Bytes32, CoinStateUpdate, Message, NewPeakWallet, ProtocolMessageTypes}, + protocol::{ + Bytes32, CoinStateFilters, CoinStateUpdate, Message, NewPeakWallet, ProtocolMessageTypes, + }, traits::Streamable, }; use chia_wallet_sdk::{ClientError, Connector, Network, MAINNET_CONSTANTS, TESTNET11_CONSTANTS}; @@ -53,6 +55,7 @@ pub struct SyncManager { transaction_queue_task: Option>>, offer_queue_task: Option>>, pending_coin_subscriptions: Vec, + pending_puzzle_subscriptions: Vec, } impl fmt::Debug for SyncManager { @@ -124,6 +127,7 @@ impl SyncManager { transaction_queue_task: None, offer_queue_task: None, pending_coin_subscriptions: Vec::new(), + pending_puzzle_subscriptions: Vec::new(), }; (manager, command_sender, event_receiver) @@ -172,6 +176,9 @@ impl SyncManager { SyncCommand::SubscribeCoins { coin_ids } => { self.pending_coin_subscriptions.extend(coin_ids); } + SyncCommand::SubscribePuzzles { puzzle_hashes } => { + self.pending_puzzle_subscriptions.extend(puzzle_hashes); + } SyncCommand::ConnectionClosed(ip) => { self.state .lock() @@ -190,24 +197,43 @@ impl SyncManager { } async fn subscribe(&mut self) { - if self.pending_coin_subscriptions.is_empty() { + if self.pending_coin_subscriptions.is_empty() + && self.pending_puzzle_subscriptions.is_empty() + { return; } if let InitialWalletSync::Subscribed(ip) = self.initial_wallet_sync { if let Some(info) = self.state.lock().await.peer(ip) { - // TODO: Handle cases - timeout( - Duration::from_secs(3), - info.peer.subscribe_coins( - mem::take(&mut self.pending_coin_subscriptions), - None, - self.network.genesis_challenge, - ), - ) - .await - .map(Result::ok) - .ok(); + if !self.pending_coin_subscriptions.is_empty() { + // TODO: Handle cases + timeout( + Duration::from_secs(3), + info.peer.subscribe_coins( + mem::take(&mut self.pending_coin_subscriptions), + None, + self.network.genesis_challenge, + ), + ) + .await + .map(Result::ok) + .ok(); + } + + if !self.pending_puzzle_subscriptions.is_empty() { + timeout( + Duration::from_secs(15), + info.peer.subscribe_puzzles( + mem::take(&mut self.pending_puzzle_subscriptions), + None, + self.network.genesis_challenge, + CoinStateFilters::new(true, true, true, 0), + ), + ) + .await + .map(Result::ok) + .ok(); + } } } } diff --git a/crates/sage-wallet/src/sync_manager/sync_command.rs b/crates/sage-wallet/src/sync_manager/sync_command.rs index 9d1e633b..291ce7bb 100644 --- a/crates/sage-wallet/src/sync_manager/sync_command.rs +++ b/crates/sage-wallet/src/sync_manager/sync_command.rs @@ -24,6 +24,9 @@ pub enum SyncCommand { SubscribeCoins { coin_ids: Vec, }, + SubscribePuzzles { + puzzle_hashes: Vec, + }, ConnectionClosed(IpAddr), SetTargetPeers(usize), SetDiscoverPeers(bool), diff --git a/crates/sage/src/endpoints/actions.rs b/crates/sage/src/endpoints/actions.rs index 9dc475be..6eed5708 100644 --- a/crates/sage/src/endpoints/actions.rs +++ b/crates/sage/src/endpoints/actions.rs @@ -1,8 +1,13 @@ +use chia::{ + bls::master_to_wallet_hardened_intermediate, + puzzles::{standard::StandardArgs, DeriveSynthetic}, +}; use sage_api::{ - RemoveCat, RemoveCatResponse, UpdateCat, UpdateCatResponse, UpdateDid, UpdateDidResponse, - UpdateNft, UpdateNftResponse, + IncreaseDerivationIndex, IncreaseDerivationIndexResponse, RemoveCat, RemoveCatResponse, + UpdateCat, UpdateCatResponse, UpdateDid, UpdateDidResponse, UpdateNft, UpdateNftResponse, }; use sage_database::{CatRow, DidRow}; +use sage_wallet::SyncCommand; use crate::{parse_asset_id, parse_did_id, parse_nft_id, Error, Result, Sage}; @@ -69,4 +74,64 @@ impl Sage { Ok(UpdateNftResponse {}) } + + pub async fn increase_derivation_index( + &self, + req: IncreaseDerivationIndex, + ) -> Result { + let wallet = self.wallet()?; + + let derivations = if req.hardened { + let (_mnemonic, Some(master_sk)) = + self.keychain.extract_secrets(wallet.fingerprint, b"")? + else { + return Err(Error::NoSigningKey); + }; + + let mut tx = wallet.db.tx().await?; + + let start = tx.derivation_index(true).await?; + let intermediate_sk = master_to_wallet_hardened_intermediate(&master_sk); + + let mut derivations = Vec::new(); + + for index in start..req.index { + let synthetic_key = intermediate_sk + .derive_hardened(index) + .derive_synthetic() + .public_key(); + + let p2_puzzle_hash = StandardArgs::curry_tree_hash(synthetic_key).into(); + + tx.insert_derivation(p2_puzzle_hash, index, true, synthetic_key) + .await?; + + derivations.push(p2_puzzle_hash); + } + + tx.commit().await?; + + derivations + } else { + let mut tx = wallet.db.tx().await?; + + let start = tx.derivation_index(false).await?; + + let derivations = wallet + .insert_unhardened_derivations(&mut tx, start..req.index) + .await?; + + tx.commit().await?; + + derivations + }; + + self.command_sender + .send(SyncCommand::SubscribePuzzles { + puzzle_hashes: derivations, + }) + .await?; + + Ok(IncreaseDerivationIndexResponse {}) + } } diff --git a/crates/sage/src/endpoints/keys.rs b/crates/sage/src/endpoints/keys.rs index c43483ad..c17014ec 100644 --- a/crates/sage/src/endpoints/keys.rs +++ b/crates/sage/src/endpoints/keys.rs @@ -66,6 +66,18 @@ impl Sage { sqlx::query!("DELETE FROM `offers`").execute(&pool).await?; } + if req.delete_unhardened_derivations { + sqlx::query!("DELETE FROM `derivations` WHERE `hardened` = 0") + .execute(&pool) + .await?; + } + + if req.delete_hardened_derivations { + sqlx::query!("DELETE FROM `derivations` WHERE `hardened` = 1") + .execute(&pool) + .await?; + } + if login { self.config.app.active_fingerprint = Some(req.fingerprint); self.save_config()?; diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 59e34f73..ec7db1c2 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -466,6 +466,15 @@ pub async fn update_nft(state: State<'_, AppState>, req: UpdateNft) -> Result, + req: IncreaseDerivationIndex, +) -> Result { + Ok(state.lock().await.increase_derivation_index(req).await?) +} + #[command] #[specta] pub async fn get_peers(state: State<'_, AppState>, req: GetPeers) -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fc4e1951..83d07954 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -83,6 +83,7 @@ pub fn run() { commands::remove_cat, commands::update_did, commands::update_nft, + commands::increase_derivation_index, commands::get_peers, commands::add_peer, commands::remove_peer, diff --git a/src/bindings.ts b/src/bindings.ts index 1719994e..54bb8e40 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -203,6 +203,9 @@ async updateDid(req: UpdateDid) : Promise { async updateNft(req: UpdateNft) : Promise { return await TAURI_INVOKE("update_nft", { req }); }, +async increaseDerivationIndex(req: IncreaseDerivationIndex) : Promise { + return await TAURI_INVOKE("increase_derivation_index", { req }); +}, async getPeers(req: GetPeers) : Promise { return await TAURI_INVOKE("get_peers", { req }); }, @@ -326,6 +329,8 @@ export type ImportKey = { name: string; key: string; save_secrets?: boolean; log export type ImportKeyResponse = { fingerprint: number } export type ImportOffer = { offer: string } export type ImportOfferResponse = Record +export type IncreaseDerivationIndex = { hardened: boolean; index: number } +export type IncreaseDerivationIndexResponse = Record export type IssueCat = { name: string; ticker: string; amount: Amount; fee: Amount; auto_submit?: boolean } export type KeyInfo = { name: string; fingerprint: number; public_key: string; kind: KeyKind; has_secrets: boolean } export type KeyKind = "bls" @@ -359,7 +364,7 @@ export type RemovePeer = { ip: string; ban: boolean } export type RemovePeerResponse = Record export type RenameKey = { fingerprint: number; name: string } export type RenameKeyResponse = Record -export type Resync = { fingerprint: number; delete_offer_files?: boolean } +export type Resync = { fingerprint: number; delete_offer_files?: boolean; delete_unhardened_derivations?: boolean; delete_hardened_derivations?: boolean } export type ResyncResponse = Record export type SecretKeyInfo = { mnemonic: string | null; secret_key: string } export type SendCat = { asset_id: string; address: string; amount: Amount; fee: Amount; memos?: string[]; auto_submit?: boolean } diff --git a/src/pages/Addresses.tsx b/src/pages/Addresses.tsx index 9a9b1baa..9c6c673e 100644 --- a/src/pages/Addresses.tsx +++ b/src/pages/Addresses.tsx @@ -9,11 +9,32 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { useErrors } from '@/hooks/useErrors'; +import { zodResolver } from '@hookform/resolvers/zod'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; +import { LoaderCircleIcon } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; import { commands, events } from '../bindings'; import AddressList from '../components/AddressList'; import { useWalletState } from '../state'; @@ -25,6 +46,8 @@ export default function Addresses() { const [hardened, setHardened] = useState(false); const [addresses, setAddresses] = useState([]); + const [deriveOpen, setDeriveOpen] = useState(false); + const [pending, setPending] = useState(false); const updateAddresses = useCallback(() => { commands @@ -51,6 +74,43 @@ export default function Addresses() { const derivationIndex = addresses.length; + const schema = z.object({ + index: z.string().refine((value) => { + const num = parseInt(value); + + if ( + isNaN(num) || + !isFinite(num) || + num < derivationIndex || + num > 100000 || + Math.floor(num) != num + ) + return false; + + return true; + }), + }); + + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: { + index: derivationIndex.toString(), + }, + }); + + const handler = (values: z.infer) => { + setPending(true); + + commands + .increaseDerivationIndex({ index: parseInt(values.index), hardened }) + .then(() => { + setDeriveOpen(false); + updateAddresses(); + }) + .catch(addError) + .finally(() => setPending(false)); + }; + return ( <>
@@ -91,8 +151,12 @@ export default function Addresses() {
- The current derivation index is {derivationIndex} -
@@ -100,6 +164,68 @@ export default function Addresses() { + + + + + + Increase Derivation Index + + + + Increase the derivation index to generate new addresses. + Setting this too high can cause issues, and it can't be + reversed without resyncing the wallet. + + + +
+ + ( + + + Derivation Index + + + + + + + )} + /> + + + + + + +
+
); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index dfab43c5..c6a59d19 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -21,6 +21,8 @@ import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { Switch } from '@/components/ui/switch'; import { useErrors } from '@/hooks/useErrors'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; import { EraserIcon, EyeIcon, @@ -36,8 +38,6 @@ import { useNavigate } from 'react-router-dom'; import { commands, KeyInfo, SecretKeyInfo } from '../bindings'; import Container from '../components/Container'; import { loginAndUpdateState } from '../state'; -import { Trans } from '@lingui/react/macro'; -import { t } from '@lingui/core/macro'; export default function Login() { const navigate = useNavigate(); @@ -145,12 +145,16 @@ function WalletItem({ network, info, keys, setKeys }: WalletItemProps) { const [isResyncOpen, setResyncOpen] = useState(false); const [deleteOffers, setDeleteOffers] = useState(false); + const [deleteUnhardened, setDeleteUnhardened] = useState(false); + const [deleteHardened, setDeleteHardened] = useState(false); const resyncSelf = () => { commands .resync({ fingerprint: info.fingerprint, delete_offer_files: deleteOffers, + delete_unhardened_derivations: deleteUnhardened, + delete_hardened_derivations: deleteHardened, }) .catch(addError) .finally(() => setResyncOpen(false)); @@ -330,6 +334,26 @@ function WalletItem({ network, info, keys, setKeys }: WalletItemProps) { onCheckedChange={(value) => setDeleteOffers(value)} /> +
+ + setDeleteUnhardened(value)} + /> +
+
+ + setDeleteHardened(value)} + /> +
From 3abaff0adb03afc23c253a506562b1685cc1c2ce Mon Sep 17 00:00:00 2001 From: Rigidity Date: Fri, 3 Jan 2025 10:07:53 -0500 Subject: [PATCH 6/9] Remove Rayon dependency --- Cargo.lock | 40 ----------------------------------- Cargo.toml | 1 - crates/sage-wallet/Cargo.toml | 1 - 3 files changed, 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33155e3c..5c75c021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,25 +1487,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -4515,26 +4496,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "rcgen" version = "0.13.1" @@ -4971,7 +4932,6 @@ dependencies = [ "futures-util", "indexmap 2.6.0", "itertools 0.13.0", - "rayon", "reqwest", "sage-database", "serde", diff --git a/Cargo.toml b/Cargo.toml index 95ed2097..1336dc16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,6 @@ itertools = "0.13.0" anyhow = "1.0.86" thiserror = "1.0.63" hex-literal = "0.4.1" -rayon = "1.10.0" once_cell = "1.19.0" num-traits = "0.2.19" paste = "1.0.15" diff --git a/crates/sage-wallet/Cargo.toml b/crates/sage-wallet/Cargo.toml index e0107376..6c0da4db 100644 --- a/crates/sage-wallet/Cargo.toml +++ b/crates/sage-wallet/Cargo.toml @@ -25,7 +25,6 @@ tokio = { workspace = true, features = ["time"] } itertools = { workspace = true } futures-util = { workspace = true } futures-lite = { workspace = true } -rayon = { workspace = true } reqwest = { workspace = true, default-features = false, features = ["http2", "rustls-tls-webpki-roots", "json"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } From 7ceefd341686bb02389d00006bc472faf490e794 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Fri, 3 Jan 2025 17:15:42 -0500 Subject: [PATCH 7/9] new ui --- crates/sage-api/src/requests/keys.rs | 2 + crates/sage/src/endpoints/keys.rs | 6 +- src/bindings.ts | 2 +- src/pages/ImportWallet.tsx | 183 +++++++++++++++++---------- 4 files changed, 120 insertions(+), 73 deletions(-) diff --git a/crates/sage-api/src/requests/keys.rs b/crates/sage-api/src/requests/keys.rs index 416b0667..4bfedbe8 100644 --- a/crates/sage-api/src/requests/keys.rs +++ b/crates/sage-api/src/requests/keys.rs @@ -45,6 +45,8 @@ pub struct GenerateMnemonicResponse { pub struct ImportKey { pub name: String, pub key: String, + #[serde(default)] + pub derivation_index: u32, #[serde(default = "yes")] pub save_secrets: bool, #[serde(default = "yes")] diff --git a/crates/sage/src/endpoints/keys.rs b/crates/sage/src/endpoints/keys.rs index c17014ec..3a92047f 100644 --- a/crates/sage/src/endpoints/keys.rs +++ b/crates/sage/src/endpoints/keys.rs @@ -17,7 +17,6 @@ use sage_api::{ ImportKeyResponse, KeyInfo, KeyKind, Login, LoginResponse, Logout, LogoutResponse, RenameKey, RenameKeyResponse, Resync, ResyncResponse, SecretKeyInfo, }; -use sage_config::WalletConfig; use sage_database::Database; use crate::{Error, Result, Sage}; @@ -153,9 +152,8 @@ impl Sage { let mut tx = db.tx().await?; let intermediate_unhardened_pk = master_to_wallet_unhardened_intermediate(&master_pk); - let batch_size = WalletConfig::default().derivation_batch_size; - for index in 0..batch_size { + for index in 0..req.derivation_index { let synthetic_key = intermediate_unhardened_pk .derive_unhardened(index) .derive_synthetic(); @@ -167,7 +165,7 @@ impl Sage { if let Some(master_sk) = master_sk { let intermediate_hardened_sk = master_to_wallet_hardened_intermediate(&master_sk); - for index in 0..batch_size { + for index in 0..req.derivation_index { let synthetic_key = intermediate_hardened_sk .derive_hardened(index) .derive_synthetic() diff --git a/src/bindings.ts b/src/bindings.ts index 54bb8e40..0d10ccec 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -325,7 +325,7 @@ export type GetTransactions = { offset: number; limit: number } export type GetTransactionsResponse = { transactions: TransactionRecord[]; total: number } export type GetXchCoins = Record export type GetXchCoinsResponse = { coins: CoinRecord[] } -export type ImportKey = { name: string; key: string; save_secrets?: boolean; login?: boolean } +export type ImportKey = { name: string; key: string; derivation_index?: number; save_secrets?: boolean; login?: boolean } export type ImportKeyResponse = { fingerprint: number } export type ImportOffer = { offer: string } export type ImportOfferResponse = Record diff --git a/src/pages/ImportWallet.tsx b/src/pages/ImportWallet.tsx index 43d21465..708bb2e8 100644 --- a/src/pages/ImportWallet.tsx +++ b/src/pages/ImportWallet.tsx @@ -1,4 +1,5 @@ import Header from '@/components/Header'; +import SafeAreaView from '@/components/SafeAreaView'; import { Button } from '@/components/ui/button'; import { Form, @@ -13,97 +14,143 @@ import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { useErrors } from '@/hooks/useErrors'; import { zodResolver } from '@hookform/resolvers/zod'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { LoaderCircleIcon } from 'lucide-react'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import * as z from 'zod'; import { commands } from '../bindings'; import Container from '../components/Container'; import { fetchState } from '../state'; -import SafeAreaView from '@/components/SafeAreaView'; -import { Trans } from '@lingui/react/macro'; -import { t } from '@lingui/core/macro'; export default function ImportWallet() { const navigate = useNavigate(); + const { addError } = useErrors(); + const [pending, setPending] = useState(false); + + const formSchema = z.object({ + name: z.string(), + key: z.string(), + addresses: z.string().refine((value) => { + const num = parseInt(value); + + return ( + isFinite(num) && + Math.floor(num) === num && + !isNaN(num) && + num >= 0 && + num <= 100000 + ); + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + addresses: '0', + }, + }); + const submit = (values: z.infer) => { + setPending(true); + commands - .importKey({ name: values.walletName, key: values.walletKey }) + .importKey({ + name: values.name, + key: values.key, + derivation_index: parseInt(values.addresses), + }) .then(fetchState) .then(() => navigate('/wallet')) - .catch(addError); + .catch(addError) + .finally(() => setPending(false)); }; return (
navigate('/')} /> - - - - ); -} +
+ + ( + + + Wallet Name + + + + + + + )} + /> -const formSchema = z.object({ - walletName: z.string(), - walletKey: z.string(), -}); + ( + + + Wallet Key + + +