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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 18 additions & 28 deletions cktap-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,6 @@ impl From<rust_cktap::FromSliceError> for KeyError {
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ChainCodeError {
#[error("Invalid length {len}, must be 32 bytes")]
InvalidLength { len: u64 },
}

impl From<Vec<u8>> for ChainCodeError {
fn from(value: Vec<u8>) -> Self {
ChainCodeError::InvalidLength {
len: value.len() as u64,
}
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum PsbtError {
#[error("Could not parse psbt: {msg}")]
Parse { msg: String },
}

impl From<rust_cktap::PsbtParseError> for PsbtError {
fn from(value: rust_cktap::PsbtParseError) -> Self {
PsbtError::Parse {
msg: value.to_string(),
}
}
}

/// Errors returned by the CkTap card.
#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)]
pub enum CardError {
Expand Down Expand Up @@ -319,6 +291,10 @@ pub enum SignPsbtError {
},
#[error("Witness program error: {msg}")]
WitnessProgram { msg: String },
#[error("Error in internal PSBT data structure: {msg}")]
PsbtEncoding { msg: String },
#[error("Error in PSBT Base64 encoding: {msg}")]
Base64Encoding { msg: String },
}

impl From<rust_cktap::SignPsbtError> for SignPsbtError {
Expand Down Expand Up @@ -350,6 +326,20 @@ impl From<rust_cktap::SignPsbtError> for SignPsbtError {
}
}

impl From<rust_cktap::PsbtParseError> for SignPsbtError {
fn from(value: rust_cktap::PsbtParseError) -> SignPsbtError {
match value {
rust_cktap::PsbtParseError::PsbtEncoding(err) => SignPsbtError::PsbtEncoding {
msg: err.to_string(),
},
rust_cktap::PsbtParseError::Base64Encoding(err) => SignPsbtError::Base64Encoding {
msg: err.to_string(),
},
_ => panic!("Unexpected error: {value:?}"),
}
}
}

/// Errors returned by the `change` command.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error, uniffi::Error)]
pub enum ChangeError {
Expand Down
109 changes: 4 additions & 105 deletions cktap-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ mod tap_signer;

uniffi::setup_scaffolding!();

use crate::error::{
CertsError, ChainCodeError, CkTapError, KeyError, PsbtError, ReadError, StatusError,
};
use crate::error::{CertsError, CkTapError, ReadError, StatusError};
use crate::sats_card::SatsCard;
use crate::sats_chip::SatsChip;
use crate::tap_signer::TapSigner;
use futures::lock::Mutex;
use rust_cktap::Network;
use rust_cktap::shared::FactoryRootKey;
use rust_cktap::shared::{Certificate, Read};
use std::fmt::{Debug, Display, Formatter};
use std::str::FromStr;
use std::fmt::Debug;
use std::sync::Arc;

#[uniffi::export(callback_interface)]
Expand All @@ -43,103 +39,6 @@ impl rust_cktap::CkTransport for CkTransportWrapper {
}
}

#[derive(uniffi::Object, Clone, Eq, PartialEq)]
pub struct PrivateKey {
inner: rust_cktap::PrivateKey,
}

#[uniffi::export]
impl PrivateKey {
#[uniffi::constructor]
pub fn from(data: Vec<u8>) -> Result<Self, KeyError> {
Ok(Self {
inner: rust_cktap::PrivateKey::from_slice(data.as_slice(), Network::Bitcoin)
.map_err(|e| KeyError::Secp256k1 { msg: e.to_string() })?,
})
}

pub fn to_bytes(&self) -> Vec<u8> {
self.inner.to_bytes()
}
}

#[derive(uniffi::Object, Clone, Eq, PartialEq)]
pub struct PublicKey {
inner: rust_cktap::PublicKey,
}

#[uniffi::export]
impl PublicKey {
#[uniffi::constructor]
pub fn from(data: Vec<u8>) -> Result<Self, KeyError> {
Ok(Self {
inner: rust_cktap::PublicKey::from_slice(data.as_slice())
.map_err(|e| KeyError::Secp256k1 { msg: e.to_string() })?,
})
}

pub fn to_bytes(&self) -> Vec<u8> {
self.inner.to_bytes()
}
}

#[derive(uniffi::Object, Clone, Eq, PartialEq)]
pub struct ChainCode {
inner: rust_cktap::ChainCode,
}

#[uniffi::export]
impl ChainCode {
#[uniffi::constructor]
pub fn from_bytes(data: Vec<u8>) -> Result<Self, ChainCodeError> {
let data: [u8; 32] = data.try_into()?;
Ok(Self {
inner: rust_cktap::ChainCode::from(data),
})
}

pub fn to_bytes(&self) -> Vec<u8> {
self.inner.to_bytes().to_vec()
}
}

#[derive(uniffi::Object, Clone, Eq, PartialEq)]
pub struct Psbt {
inner: rust_cktap::Psbt,
}

#[uniffi::export]
impl Psbt {
#[uniffi::constructor]
pub fn from_base64(data: String) -> Result<Self, PsbtError> {
Ok(Self {
inner: rust_cktap::Psbt::from_str(&data)?,
})
}

pub fn to_base64(&self) -> String {
self.inner.to_string()
}
}

#[derive(uniffi::Object, Clone, Eq, PartialEq)]
pub struct Xpub {
inner: rust_cktap::Xpub,
}

#[uniffi::export]
impl Xpub {
pub fn encode(&self) -> Vec<u8> {
self.inner.encode().to_vec()
}
}

impl Display for Xpub {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}

#[derive(uniffi::Enum)]
pub enum CkTapCard {
SatsCard(Arc<SatsCard>),
Expand Down Expand Up @@ -170,10 +69,10 @@ pub async fn to_cktap(transport: Box<dyn CkTransport>) -> Result<CkTapCard, Stat
async fn read(
card: &mut (impl Read + Send + Sync),
cvc: Option<String>,
) -> Result<Vec<u8>, ReadError> {
) -> Result<String, ReadError> {
card.read(cvc)
.await
.map(|pk| pk.to_bytes())
.map(|pk| pk.to_string())
.map_err(ReadError::from)
}

Expand Down
107 changes: 58 additions & 49 deletions cktap-ffi/src/sats_card.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) 2025 rust-cktap contributors
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::check_cert;
use crate::error::{
CertsError, CkTapError, DeriveError, DumpError, ReadError, SignPsbtError, UnsealError,
};
use crate::{ChainCode, PrivateKey, Psbt, PublicKey, check_cert, read};
use futures::lock::Mutex;
use rust_cktap::shared::{Authentication, Nfc, Wait};
use std::sync::Arc;
use rust_cktap::descriptor::Wpkh;
use rust_cktap::shared::{Authentication, Nfc, Read, Wait};
use rust_cktap::{Psbt, rand_chaincode};
use std::str::FromStr;

#[derive(uniffi::Object)]
pub struct SatsCard(pub Mutex<rust_cktap::SatsCard>);
Expand All @@ -20,43 +22,50 @@ pub struct SatsCardStatus {
pub active_slot: u8,
pub num_slots: u8,
pub addr: Option<String>,
pub pubkey: Vec<u8>,
pub pubkey: String,
pub auth_delay: Option<u8>,
}

#[derive(uniffi::Record, Clone)]
pub struct UnsealedSlot {
slot: u8,
privkey: Option<Arc<PrivateKey>>,
pubkey: Arc<PublicKey>,
pub struct SlotDetails {
privkey: Option<String>,
pubkey: String,
pubkey_descriptor: String,
}

#[uniffi::export]
impl SatsCard {
pub async fn status(&self) -> SatsCardStatus {
let card = self.0.lock().await;
let pubkey = card.pubkey().to_string();
SatsCardStatus {
proto: card.proto as u64,
ver: card.ver().to_string(),
birth: card.birth as u64,
active_slot: card.slots.0,
num_slots: card.slots.1,
addr: card.addr.clone(),
pubkey: card.pubkey().to_bytes(),
pubkey,
auth_delay: card.auth_delay().map(|d| d as u8),
}
}

/// Get the current active slot's receive address
pub async fn address(&self) -> Result<String, ReadError> {
let mut card = self.0.lock().await;
card.address().await.map_err(ReadError::from)
let address = card.address().await?;
Ok(address.to_string())
}

pub async fn read(&self) -> Result<Vec<u8>, ReadError> {
/// Get the current active slot's wpkh public key descriptor
pub async fn read(&self) -> Result<String, ReadError> {
let mut card = self.0.lock().await;
read(&mut *card, None).await
let pubkey = card.read(None).await?;
let pubkey_desc = format!("{}", Wpkh::new(pubkey).unwrap());
Ok(pubkey_desc)
}

/// Wait 15 seconds or until auth delay timeout is done
pub async fn wait(&self) -> Result<(), CkTapError> {
let mut card = self.0.lock().await;
// if auth delay call wait
Expand All @@ -66,68 +75,68 @@ impl SatsCard {
Ok(())
}

/// Verify the card has authentic Coinkite root certificate
pub async fn check_cert(&self) -> Result<(), CertsError> {
let mut card = self.0.lock().await;
check_cert(&mut *card).await
}

pub async fn new_slot(
&self,
slot: u8,
chain_code: Option<Arc<ChainCode>>,
cvc: String,
) -> Result<u8, CkTapError> {
/// Open a new slot, it will be the current active but must be unused (no address)
pub async fn new_slot(&self, cvc: String) -> Result<u8, DeriveError> {
let mut card = self.0.lock().await;
let chain_code = chain_code.map(|cc| cc.inner);
card.new_slot(slot, chain_code, &cvc)
let (active_slot, _) = card.slots;
let new_slot_chain_code = rand_chaincode();
let new_slot = card
.new_slot(active_slot, Some(new_slot_chain_code), &cvc)
.await
.map_err(CkTapError::from)
}

pub async fn derive(&self) -> Result<ChainCode, DeriveError> {
let mut card = self.0.lock().await;
let chain_code = card.derive().await.map(|cc| ChainCode { inner: cc })?;
Ok(chain_code)
.map_err(CkTapError::from)?;
let derive_chain_code = card.derive().await?;
if derive_chain_code != new_slot_chain_code {
return Err(DeriveError::InvalidChainCode {
msg: "Chain code used by derive doesn't match new slot chain code".to_string(),
});
}
Ok(new_slot)
}

pub async fn unseal(&self, slot: u8, cvc: String) -> Result<UnsealedSlot, UnsealError> {
/// Unseal currently active slot
pub async fn unseal(&self, cvc: String) -> Result<SlotDetails, UnsealError> {
let mut card = self.0.lock().await;
let (privkey, pubkey) = card.unseal(slot, &cvc).await?;
let pubkey = Arc::new(PublicKey { inner: pubkey });
let privkey = Some(Arc::new(PrivateKey { inner: privkey }));
Ok(UnsealedSlot {
slot,
pubkey,
privkey,
let active_slot = card.slots.0;
let (privkey, pubkey) = card.unseal(active_slot, &cvc).await?;
Ok(SlotDetails {
privkey: Some(privkey.to_string()),
pubkey: pubkey.to_string(),
pubkey_descriptor: format!("{}", Wpkh::new(pubkey).unwrap()),
})
}

pub async fn dump(&self, slot: u8, cvc: Option<String>) -> Result<UnsealedSlot, DumpError> {
/// This is only needed for debugging, use `sign_psbt` for signing
/// If no CVC given only pubkey and pubkey descriptor returned.
pub async fn dump(&self, slot: u8, cvc: Option<String>) -> Result<SlotDetails, DumpError> {
let mut card = self.0.lock().await;
let (privkey, pubkey) = card.dump(slot, cvc).await?;
let pubkey = Arc::new(PublicKey { inner: pubkey });
let privkey = privkey.map(|sk| Arc::new(PrivateKey { inner: sk }));
Ok(UnsealedSlot {
slot,
pubkey,
privkey,
Ok(SlotDetails {
privkey: privkey.map(|sk| sk.to_string()),
pubkey: pubkey.to_string(),
pubkey_descriptor: format!("{}", Wpkh::new(pubkey).unwrap()),
})
}

/// Sign PSBT, base64 encoded
pub async fn sign_psbt(
&self,
slot: u8,
psbt: Arc<Psbt>,
psbt: String,
cvc: String,
) -> Result<Psbt, SignPsbtError> {
) -> Result<String, SignPsbtError> {
let mut card = self.0.lock().await;
let psbt = card
.sign_psbt(slot, (*psbt).clone().inner, &cvc)
.await
.map(|psbt| Psbt { inner: psbt })?;
Ok(psbt)
let psbt = Psbt::from_str(&psbt)?;
let signed_psbt = card.sign_psbt(slot, psbt, &cvc).await?;
Ok(signed_psbt.to_string())
}

/// Return the same URL as given with a NFC tap.
pub async fn nfc(&self) -> Result<String, CkTapError> {
let mut card = self.0.lock().await;
let url = card.nfc().await?;
Expand Down
Loading
Loading