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
106 changes: 106 additions & 0 deletions bindings/ios/bitkitcore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21557,6 +21557,92 @@ extension Scanner: Codable {}




public enum SeedQrError: Swift.Error {



case InvalidStandardPayload
case InvalidCompactPayload
case InvalidMnemonic
}


#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeSeedQrError: FfiConverterRustBuffer {
typealias SwiftType = SeedQrError

public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SeedQrError {
let variant: Int32 = try readInt(&buf)
switch variant {




case 1: return .InvalidStandardPayload
case 2: return .InvalidCompactPayload
case 3: return .InvalidMnemonic

default: throw UniffiInternalError.unexpectedEnumCase
}
}

public static func write(_ value: SeedQrError, into buf: inout [UInt8]) {
switch value {





case .InvalidStandardPayload:
writeInt(&buf, Int32(1))


case .InvalidCompactPayload:
writeInt(&buf, Int32(2))


case .InvalidMnemonic:
writeInt(&buf, Int32(3))

}
}
}


#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeSeedQrError_lift(_ buf: RustBuffer) throws -> SeedQrError {
return try FfiConverterTypeSeedQrError.lift(buf)
}

#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeSeedQrError_lower(_ value: SeedQrError) -> RustBuffer {
return FfiConverterTypeSeedQrError.lower(value)
}


extension SeedQrError: Equatable, Hashable {}

extension SeedQrError: Codable {}




extension SeedQrError: Foundation.LocalizedError {
public var errorDescription: String? {
String(reflecting: self)
}
}




// Note that we don't yet support `indirect` for enums.
// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.

Expand Down Expand Up @@ -25868,6 +25954,20 @@ public func decode(invoice: String)async throws -> Scanner {
errorHandler: FfiConverterTypeDecodingError_lift
)
}
public func decodeCompactSeedQr(entropy: Data)throws -> String {
return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeSeedQrError_lift) {
uniffi_bitkitcore_fn_func_decode_compact_seed_qr(
FfiConverterData.lower(entropy),$0
)
})
}
public func decodeStandardSeedQr(payload: String)throws -> String {
return try FfiConverterString.lift(try rustCallWithError(FfiConverterTypeSeedQrError_lift) {
uniffi_bitkitcore_fn_func_decode_standard_seed_qr(
FfiConverterString.lower(payload),$0
)
})
}
public func deleteActivitiesByWalletId(walletId: String)throws -> UInt32 {
return try FfiConverterUInt32.lift(try rustCallWithError(FfiConverterTypeActivityError_lift) {
uniffi_bitkitcore_fn_func_delete_activities_by_wallet_id(
Expand Down Expand Up @@ -28099,6 +28199,12 @@ private let initializationResult: InitializationResult = {
if (uniffi_bitkitcore_checksum_func_decode() != 28437) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_bitkitcore_checksum_func_decode_compact_seed_qr() != 58531) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_bitkitcore_checksum_func_decode_standard_seed_qr() != 53117) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_bitkitcore_checksum_func_delete_activities_by_wallet_id() != 15848) {
return InitializationResult.apiChecksumMismatch
}
Expand Down
22 changes: 22 additions & 0 deletions bindings/ios/bitkitcoreFFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,16 @@ RustBuffer uniffi_bitkitcore_fn_func_create_withdraw_callback_url(RustBuffer k1,
uint64_t uniffi_bitkitcore_fn_func_decode(RustBuffer invoice
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DECODE_COMPACT_SEED_QR
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DECODE_COMPACT_SEED_QR
RustBuffer uniffi_bitkitcore_fn_func_decode_compact_seed_qr(RustBuffer entropy, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DECODE_STANDARD_SEED_QR
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DECODE_STANDARD_SEED_QR
RustBuffer uniffi_bitkitcore_fn_func_decode_standard_seed_qr(RustBuffer payload, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DELETE_ACTIVITIES_BY_WALLET_ID
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_DELETE_ACTIVITIES_BY_WALLET_ID
uint32_t uniffi_bitkitcore_fn_func_delete_activities_by_wallet_id(RustBuffer wallet_id, RustCallStatus *_Nonnull out_status
Expand Down Expand Up @@ -2200,6 +2210,18 @@ uint16_t uniffi_bitkitcore_checksum_func_create_withdraw_callback_url(void
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DECODE
uint16_t uniffi_bitkitcore_checksum_func_decode(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DECODE_COMPACT_SEED_QR
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DECODE_COMPACT_SEED_QR
uint16_t uniffi_bitkitcore_checksum_func_decode_compact_seed_qr(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DECODE_STANDARD_SEED_QR
#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DECODE_STANDARD_SEED_QR
uint16_t uniffi_bitkitcore_checksum_func_decode_standard_seed_qr(void

);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_DELETE_ACTIVITIES_BY_WALLET_ID
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub use modules::boltz as boltz_swaps;
pub use modules::lnurl;
pub use modules::onchain;
pub use modules::scanner::{DecodingError, LnurlPayData, Scanner};
pub use modules::seedqr::{decode_compact_seed_qr, decode_standard_seed_qr, SeedQrError};

use bip39::Mnemonic;
use bitcoin::bip32::Xpriv;
Expand Down
1 change: 1 addition & 0 deletions src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ pub mod lnurl;
pub mod onchain;
pub mod pubky;
pub mod scanner;
pub mod seedqr;
pub mod trezor;
pub mod ur;
12 changes: 12 additions & 0 deletions src/modules/seedqr/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use thiserror::Error;

#[derive(uniffi::Error, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum SeedQrError {
#[error("Invalid Standard SeedQR payload")]
InvalidStandardPayload,
#[error("Invalid Compact SeedQR payload")]
InvalidCompactPayload,
#[error("SeedQR contains an invalid BIP39 mnemonic")]
InvalidMnemonic,
}
42 changes: 42 additions & 0 deletions src/modules/seedqr/implementation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use super::SeedQrError;
use crate::onchain::BitcoinAddressValidator;

const COMPACT_ENTROPY_LENGTH: usize = 16;
const STANDARD_PAYLOAD_LENGTH: usize = 48;
const WORD_COUNT: usize = 12;
const WORD_INDEX_LENGTH: usize = 4;

#[uniffi::export]
pub fn decode_standard_seed_qr(payload: String) -> Result<String, SeedQrError> {
if payload.len() != STANDARD_PAYLOAD_LENGTH
|| !payload.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(SeedQrError::InvalidStandardPayload);
}

let wordlist = BitcoinAddressValidator::get_bip39_wordlist();
let mut words = Vec::with_capacity(WORD_COUNT);
for chunk in payload.as_bytes().chunks_exact(WORD_INDEX_LENGTH) {
let index = std::str::from_utf8(chunk)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|index| *index < wordlist.len())
.ok_or(SeedQrError::InvalidStandardPayload)?;
words.push(wordlist[index].as_str());
}

let mnemonic = words.join(" ");
BitcoinAddressValidator::validate_mnemonic(&mnemonic)
.map(|()| mnemonic)
.map_err(|_| SeedQrError::InvalidMnemonic)
}

#[uniffi::export]
pub fn decode_compact_seed_qr(entropy: Vec<u8>) -> Result<String, SeedQrError> {
if entropy.len() != COMPACT_ENTROPY_LENGTH {
return Err(SeedQrError::InvalidCompactPayload);
}

BitcoinAddressValidator::entropy_to_mnemonic(&entropy)
.map_err(|_| SeedQrError::InvalidCompactPayload)
}
13 changes: 13 additions & 0 deletions src/modules/seedqr/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! SeedQR decoding shared by the platform applications.
//!
//! Camera scanning and normalization of platform-specific barcode payloads
//! remain application concerns. This module accepts the canonical Standard
//! SeedQR text or Compact SeedQR entropy described by the SeedQR format.

mod errors;
mod implementation;
#[cfg(test)]
mod tests;

pub use errors::SeedQrError;
pub use implementation::{decode_compact_seed_qr, decode_standard_seed_qr};
86 changes: 86 additions & 0 deletions src/modules/seedqr/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use super::{decode_compact_seed_qr, decode_standard_seed_qr, SeedQrError};

const EXPECTED_MNEMONIC: &str =
"forum undo fragile fade shy sign arrest garment culture tube off merit";
const STANDARD_PAYLOAD: &str = "073318950739065415961602009907670428187212261116";

#[test]
fn decodes_standard_seedqr() {
assert_eq!(
decode_standard_seed_qr(STANDARD_PAYLOAD.to_string()).unwrap(),
EXPECTED_MNEMONIC
);
}

#[test]
fn rejects_standard_seedqr_with_short_length() {
let payload = STANDARD_PAYLOAD.strip_suffix('6').unwrap().to_string();

assert_eq!(
decode_standard_seed_qr(payload),
Err(SeedQrError::InvalidStandardPayload)
);
}

#[test]
fn rejects_standard_seedqr_with_overlong_length() {
let payload = format!("{STANDARD_PAYLOAD}0");

assert_eq!(
decode_standard_seed_qr(payload),
Err(SeedQrError::InvalidStandardPayload)
);
}

#[test]
fn rejects_standard_seedqr_with_invalid_checksum() {
let payload = "0000".repeat(12);

assert_eq!(
decode_standard_seed_qr(payload),
Err(SeedQrError::InvalidMnemonic)
);
}

#[test]
fn rejects_standard_seedqr_with_non_digit() {
let payload = format!("{}x", "0000".repeat(12).strip_suffix('0').unwrap());

assert_eq!(
decode_standard_seed_qr(payload),
Err(SeedQrError::InvalidStandardPayload)
);
}

#[test]
fn rejects_standard_seedqr_with_out_of_range_index() {
let payload = format!("2048{}", "0000".repeat(11));

assert_eq!(
decode_standard_seed_qr(payload),
Err(SeedQrError::InvalidStandardPayload)
);
}

#[test]
fn decodes_compact_seedqr() {
let entropy = hex::decode("5bbd9d71a8ec7990831aff359d426545").unwrap();

assert_eq!(decode_compact_seed_qr(entropy).unwrap(), EXPECTED_MNEMONIC);
}

#[test]
fn decodes_compact_seedqr_containing_null_bytes() {
assert_eq!(
decode_compact_seed_qr(vec![0; 16]).unwrap(),
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
);
}

#[test]
fn rejects_compact_seedqr_with_invalid_length() {
Comment thread
ovitrif marked this conversation as resolved.
assert_eq!(
decode_compact_seed_qr(vec![0; 15]),
Err(SeedQrError::InvalidCompactPayload)
);
}
Loading