ark-client 0.9.3

Main client library for interacting with Ark servers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
use crate::error::Error;
use bitcoin::bip32::DerivationPath;
use bitcoin::bip32::Xpriv;
use bitcoin::key::Keypair;
use bitcoin::secp256k1::Secp256k1;
use std::sync::Arc;

pub enum KeypairIndex {
    /// Increments the index and returns a new keypair
    New,
    /// Returns the last unused address
    LastUnused,
}

/// Provides keypairs for signing operations
///
/// This trait allows different key management strategies:
/// - Static keypair (single key)
/// - BIP32 HD wallet (hierarchical deterministic)
/// - Hardware wallets (future)
/// - Custom key derivation schemes
pub trait KeyProvider: Send + Sync {
    /// Get a keypair for receiving funds
    ///
    /// For static key providers, this always returns the same keypair regardless of the index.
    /// For HD wallets, behavior depends on the `keypair_index` parameter.
    ///
    /// # Arguments
    ///
    /// * `keypair_index` - Controls which keypair to return:
    ///   - `KeypairIndex::New`: Increments the internal index and returns a new keypair
    ///   - `KeypairIndex::LastUnused`: Returns the last unused keypair without incrementing
    ///
    /// # Returns
    ///
    /// A keypair to use for receiving funds
    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error>;

    /// Get a keypair for a specific BIP32 derivation path
    ///
    /// # Arguments
    ///
    /// * `path` - BIP32 derivation path as an array of child indexes
    ///
    /// # Returns
    ///
    /// A keypair derived at the specified path, or an error if derivation is not supported
    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error>;

    /// Get a keypair for a specific public key
    ///
    /// This is essential for HD wallets where you need to find the correct keypair
    /// for signing with a previously generated public key.
    ///
    /// # Arguments
    ///
    /// * `pk` - The X-only public key to find the keypair for
    ///
    /// # Returns
    ///
    /// The keypair corresponding to the public key, or an error if not found
    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error>;

    /// Get all public keys that this provider currently knows about
    ///
    /// For static key providers, this returns the single keypair's public key.
    /// For HD wallets, this returns all public keys that have been derived and cached
    /// (i.e., keys generated via `get_next_keypair`).
    ///
    /// This is useful for determining which keys are available for signing operations
    /// without having to search or derive new keys.
    ///
    /// # Returns
    ///
    /// A vector of X-only public keys known to this provider
    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error>;

    /// Returns true if this provider supports key discovery
    ///
    /// HD wallets return true since they can derive and discover previously used keys.
    /// Static key providers return false (single key, nothing to discover).
    fn supports_discovery(&self) -> bool {
        false
    }

    /// Get the derivation index for a cached public key.
    ///
    /// Returns `None` if the provider doesn't support index-based derivation
    /// or if the key is not in the cache.
    fn get_derivation_index_for_pk(&self, _pk: &bitcoin::XOnlyPublicKey) -> Option<u32> {
        None
    }

    /// Derive a keypair at a specific index without caching
    ///
    /// This is used during discovery to check keys without affecting the provider's state.
    /// Returns `None` if the provider doesn't support index-based derivation.
    ///
    /// # Arguments
    ///
    /// * `index` - The derivation index (appended to base path for HD wallets)
    fn derive_at_discovery_index(&self, _index: u32) -> Result<Option<Keypair>, Error> {
        Ok(None)
    }

    /// Cache a discovered keypair at the given index
    ///
    /// This is called after discovery determines a key is "used" (has VTXOs).
    /// Also updates next_index if index >= current next_index to avoid collisions.
    ///
    /// No-op for providers that don't support discovery.
    ///
    /// # Arguments
    ///
    /// * `index` - The derivation index
    /// * `kp` - The keypair to cache
    fn cache_discovered_keypair(&self, _index: u32, _kp: Keypair) -> Result<(), Error> {
        Ok(())
    }

    fn mark_as_used(&self, _pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error> {
        Ok(())
    }
}

/// A simple key provider that uses a static keypair
///
/// This is the simplest implementation and is backward compatible with
/// the original single-keypair design.
#[derive(Clone)]
pub struct StaticKeyProvider {
    kp: Keypair,
}

impl StaticKeyProvider {
    /// Create a new static key provider
    pub fn new(kp: Keypair) -> Self {
        Self { kp }
    }
}

impl KeyProvider for StaticKeyProvider {
    fn get_next_keypair(&self, _: KeypairIndex) -> Result<Keypair, Error> {
        // Static provider always returns the same keypair
        Ok(self.kp)
    }

    fn get_keypair_for_path(&self, _path: &[u32]) -> Result<Keypair, Error> {
        // Static provider always returns the same keypair
        Ok(self.kp)
    }

    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
        // Verify that the requested public key matches our keypair
        let our_pk = self.kp.x_only_public_key().0;
        if &our_pk == pk {
            Ok(self.kp)
        } else {
            Err(Error::ad_hoc(format!(
                "Public key mismatch: requested {pk}, but only have {our_pk}"
            )))
        }
    }

    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
        Ok(vec![self.kp.public_key().into()])
    }
}

/// A BIP32 hierarchical deterministic key provider
///
/// This provider derives keypairs from a master extended private key
/// using BIP32 derivation paths. It maintains an index counter for
/// generating new receiving addresses.
///
/// ## Example
///
/// ```rust
/// # use std::str::FromStr;
/// # use bitcoin::bip32::{Xpriv, DerivationPath};
/// # use bitcoin::Network;
/// # use crate::ark_client::KeyProvider;
/// # use ark_client::Bip32KeyProvider;
/// # use ark_client::key_provider::KeypairIndex;
///
/// fn example() -> Result<(), Box<dyn std::error::Error>> {
/// // Create from a master key with a base path (e.g., m/84'/0'/0'/0)
/// let master_key = Xpriv::from_str("xprv...")?;
/// let base_path = DerivationPath::from_str("m/84'/0'/0'/0")?;
///
/// // This will derive keys at m/84'/0'/0'/0/0, m/84'/0'/0'/0/1, etc.
/// let provider = Bip32KeyProvider::new(master_key, base_path);
///
/// // Get the next receiving keypair (increments index)
/// let kp1 = provider.get_next_keypair(KeypairIndex::New)?; // m/84'/0'/0'/0/0
/// let kp2 = provider.get_next_keypair(KeypairIndex::New)?; // m/84'/0'/0'/0/1
///
/// // Or derive a specific keypair by path
/// let custom_path = vec![84 + 0x8000_0000, 0x8000_0000, 0x8000_0000, 0, 5];
/// let kp = provider.get_keypair_for_path(&custom_path)?;
/// # Ok(())
/// # }
/// ```
pub struct Bip32KeyProvider {
    master_key: Xpriv,
    base_path: DerivationPath,
    // Using std::sync::Mutex for interior mutability across Send + Sync
    next_index: Arc<std::sync::Mutex<u32>>,
    // Cache of derived keys: pk -> (path_index, keypair, used)
    // The `used` flag indicates whether this keypair has been used (has VTXOs)
    key_cache:
        Arc<std::sync::RwLock<std::collections::HashMap<bitcoin::XOnlyPublicKey, KeyCacheValue>>>,
}

#[derive(Clone, Copy)]
pub struct KeyCacheValue {
    path_index: u32,
    kp: Keypair,
    /// Indicates whether this keypair has been used (has VTXOs).
    used: bool,
}

impl Bip32KeyProvider {
    /// Create a new BIP32 key provider
    ///
    /// # Arguments
    ///
    /// * `master_key` - The master extended private key (xpriv)
    /// * `base_path` - The base derivation path (e.g., m/84'/0'/0'/0). The provider will append
    ///   index numbers to this path.
    pub fn new(master_key: Xpriv, base_path: DerivationPath) -> Self {
        Self {
            master_key,
            base_path,
            next_index: Arc::new(std::sync::Mutex::new(0)),
            key_cache: Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
        }
    }

    /// Create a new BIP32 key provider starting from a specific index
    ///
    /// # Arguments
    ///
    /// * `master_key` - The master extended private key (xpriv)
    /// * `base_path` - The base derivation path
    /// * `start_index` - The starting index for key derivation
    pub fn new_with_index(master_key: Xpriv, base_path: DerivationPath, start_index: u32) -> Self {
        Self {
            master_key,
            base_path,
            next_index: Arc::new(std::sync::Mutex::new(start_index)),
            key_cache: Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
        }
    }

    /// Derive a keypair at the specified path
    fn derive_keypair(&self, path: &DerivationPath) -> Result<Keypair, Error> {
        let secp = Secp256k1::new();
        let derived_key = self
            .master_key
            .derive_priv(&secp, path)
            .map_err(|e| Error::ad_hoc(format!("BIP32 derivation failed: {e}")))?;

        Ok(derived_key.to_keypair(&secp))
    }

    /// Derive a keypair at base_path/index
    fn derive_at_index(&self, index: u32) -> Result<Keypair, Error> {
        use bitcoin::bip32::ChildNumber;

        let path = self.base_path.clone();
        let path = path.extend([ChildNumber::Normal { index }]);

        self.derive_keypair(&path)
    }
}

impl KeyProvider for Bip32KeyProvider {
    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
        match keypair_index {
            KeypairIndex::New => {
                // Get and increment the next index
                let index = {
                    let mut next_index = self
                        .next_index
                        .lock()
                        .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
                    let current = *next_index;
                    *next_index = next_index
                        .checked_add(1)
                        .ok_or_else(|| Error::ad_hoc("Key derivation index overflow"))?;
                    current
                };

                // Derive the keypair at this index
                let kp = self.derive_at_index(index)?;

                // Cache it for later lookup (marked as unused)
                let pk = kp.x_only_public_key().0;
                {
                    let mut cache = self
                        .key_cache
                        .write()
                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
                    cache.insert(
                        pk,
                        KeyCacheValue {
                            path_index: index,
                            kp,
                            used: false,
                        },
                    );
                }

                Ok(kp)
            }
            KeypairIndex::LastUnused => {
                // First, try to find an unused keypair in the cache
                {
                    let cache = self
                        .key_cache
                        .read()
                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;

                    // Find the unused keypair with the lowest index
                    let unused = cache
                        .values()
                        .filter(|KeyCacheValue { used, .. }| !used)
                        .min_by_key(|KeyCacheValue { path_index, .. }| *path_index);

                    if let Some(KeyCacheValue { kp, .. }) = unused {
                        return Ok(*kp);
                    }
                }

                // No unused keypair found, derive a new one
                self.get_next_keypair(KeypairIndex::New)
            }
        }
    }

    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error> {
        use bitcoin::bip32::ChildNumber;
        let child_numbers: Vec<ChildNumber> = path
            .iter()
            .map(|&n| {
                if n & 0x8000_0000 != 0 {
                    ChildNumber::Hardened {
                        index: n & 0x7FFF_FFFF,
                    }
                } else {
                    ChildNumber::Normal { index: n }
                }
            })
            .collect();
        let derivation_path = DerivationPath::from(child_numbers);
        self.derive_keypair(&derivation_path)
    }

    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
        // First check the cache
        {
            let cache = self
                .key_cache
                .read()
                .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
            if let Some(KeyCacheValue { kp, .. }) = cache.get(pk) {
                return Ok(*kp);
            }
        }

        // If not in cache, we need to search. For now, we'll search up to the current index
        let current_index = {
            let next_index = self
                .next_index
                .lock()
                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
            *next_index
        };

        // Search through derived keys up to current index
        for i in 0..current_index {
            let kp = self.derive_at_index(i)?;
            let derived_pk = kp.x_only_public_key().0;

            if &derived_pk == pk {
                // Cache it for next time (assume used since we're looking it up for signing)
                let mut cache = self
                    .key_cache
                    .write()
                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
                cache.insert(
                    derived_pk,
                    KeyCacheValue {
                        path_index: i,
                        kp,
                        used: true,
                    },
                );
                return Ok(kp);
            }
        }

        Err(Error::ad_hoc(format!(
            "Public key {pk} not found in HD wallet. \
            Searched indices 0..{current_index}. \
            The key may have been generated outside this provider."
        )))
    }

    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
        let cache = self
            .key_cache
            .read()
            .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;

        Ok(cache.keys().copied().collect())
    }

    fn supports_discovery(&self) -> bool {
        true
    }

    fn get_derivation_index_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Option<u32> {
        let cache = self.key_cache.read().ok()?;
        cache.get(pk).map(|v| v.path_index)
    }

    fn derive_at_discovery_index(&self, index: u32) -> Result<Option<Keypair>, Error> {
        self.derive_at_index(index).map(Some)
    }

    fn cache_discovered_keypair(&self, index: u32, kp: Keypair) -> Result<(), Error> {
        let pk = kp.x_only_public_key().0;

        // Add to cache (marked as used since it was discovered with VTXOs)
        {
            let mut cache = self
                .key_cache
                .write()
                .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
            cache.insert(
                pk,
                KeyCacheValue {
                    path_index: index,
                    kp,
                    used: true,
                },
            );
        }

        // Update next_index if needed (set to index + 1 if >= current)
        {
            let mut next = self
                .next_index
                .lock()
                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
            if index >= *next {
                *next = index
                    .checked_add(1)
                    .ok_or_else(|| Error::ad_hoc("Key derivation index overflow"))?;
            }
        }

        Ok(())
    }

    fn mark_as_used(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error> {
        // First check the cache
        {
            let maybe_kp = {
                let cache = self
                    .key_cache
                    .read()
                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
                cache.get(pk).copied()
            };

            match maybe_kp {
                Some(KeyCacheValue {
                    path_index,
                    kp,
                    used: false,
                }) => {
                    let mut cache = self
                        .key_cache
                        .write()
                        .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
                    cache.insert(
                        *pk,
                        KeyCacheValue {
                            path_index,
                            kp,
                            used: true,
                        },
                    );
                    return Ok(());
                }
                Some(KeyCacheValue { used: true, .. }) => {
                    // already marked as used
                    return Ok(());
                }
                _ => {
                    // no found
                }
            }
        }

        // If not in cache, we need to search. For now, we'll search up to the current index
        let current_index = {
            let next_index = self
                .next_index
                .lock()
                .map_err(|e| Error::ad_hoc(format!("Failed to lock next_index: {e}")))?;
            *next_index
        };

        // Search through derived keys up to current index
        for i in 0..current_index {
            let kp = self.derive_at_index(i)?;
            let derived_pk = kp.x_only_public_key().0;

            if &derived_pk == pk {
                // Cache it for next time (assume used since we're looking it up for signing)
                let mut cache = self
                    .key_cache
                    .write()
                    .map_err(|e| Error::ad_hoc(format!("Failed to lock key_cache: {e}")))?;
                cache.insert(
                    derived_pk,
                    KeyCacheValue {
                        path_index: i,
                        kp,
                        used: true,
                    },
                );
                return Ok(());
            }
        }

        Err(Error::ad_hoc(format!(
            "Public key {pk} not found in HD wallet. \
            Searched indices 0..{current_index}. \
            The key may have been generated outside this provider."
        )))
    }
}

// Implement KeyProvider for Arc<T> where T: KeyProvider
impl<T: KeyProvider> KeyProvider for Arc<T> {
    fn get_next_keypair(&self, keypair_index: KeypairIndex) -> Result<Keypair, Error> {
        (**self).get_next_keypair(keypair_index)
    }

    fn get_keypair_for_path(&self, path: &[u32]) -> Result<Keypair, Error> {
        (**self).get_keypair_for_path(path)
    }

    fn get_keypair_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<Keypair, Error> {
        (**self).get_keypair_for_pk(pk)
    }

    fn get_cached_pks(&self) -> Result<Vec<bitcoin::XOnlyPublicKey>, Error> {
        (**self).get_cached_pks()
    }

    fn supports_discovery(&self) -> bool {
        (**self).supports_discovery()
    }

    fn get_derivation_index_for_pk(&self, pk: &bitcoin::XOnlyPublicKey) -> Option<u32> {
        (**self).get_derivation_index_for_pk(pk)
    }

    fn derive_at_discovery_index(&self, index: u32) -> Result<Option<Keypair>, Error> {
        (**self).derive_at_discovery_index(index)
    }

    fn cache_discovered_keypair(&self, index: u32, kp: Keypair) -> Result<(), Error> {
        (**self).cache_discovered_keypair(index, kp)
    }

    fn mark_as_used(&self, pk: &bitcoin::XOnlyPublicKey) -> Result<(), Error> {
        (**self).mark_as_used(pk)
    }
}