From 2bf680cf69dfe32c175270985f572196d512f1db Mon Sep 17 00:00:00 2001 From: Soralit <44085950+soralit@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:32:13 +0800 Subject: [PATCH] feat: add psbt extend support on sdk --- include/URRegistryFFI/lib_ur_registry_ffi.h | 5 +- libs/ur-registry-ffi/README.md | 22 ++++ .../src/bitcoin/crypto_psbt_extend.rs | 111 ++++++++++++++++++ libs/ur-registry-ffi/src/bitcoin/mod.rs | 1 + 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 libs/ur-registry-ffi/src/bitcoin/crypto_psbt_extend.rs diff --git a/include/URRegistryFFI/lib_ur_registry_ffi.h b/include/URRegistryFFI/lib_ur_registry_ffi.h index 4133979..b0ee97f 100644 --- a/include/URRegistryFFI/lib_ur_registry_ffi.h +++ b/include/URRegistryFFI/lib_ur_registry_ffi.h @@ -20,6 +20,9 @@ const char* parse_hd_path(struct ExternError*, const char* hd_path); // BTC const char* generate_crypto_psbt(struct ExternError*, const char* psbt_hex); const char* parse_crypto_psbt(struct ExternError*, const char* ur_type, const char* cbor_hex); +// coin_id: 0 omits the coin field (Bitcoin); 2 LTC, 3 DOGE, 4 DASH, 145 BCH. +const char* generate_crypto_psbt_extend(struct ExternError*, const char* psbt_hex, int32_t coin_id); +const char* parse_crypto_psbt_extend(struct ExternError*, const char* ur_type, const char* cbor_hex); const char* generate_btc_sign_request(struct ExternError*, const char* request_id, const char* sign_data, const int data_type, const char* accounts, const char* origin); const char* parse_btc_signature(struct ExternError*, const char* ur_type, const char* cbor_hex); @@ -75,4 +78,4 @@ const char* generate_zcash_pczt(struct ExternError*, const char* pczt_hex); // Kaspa PSKT const char* generate_kaspa_pskt(struct ExternError*, const char* pskt_hex); -const char* parse_kaspa_pskt(struct ExternError*, const char* ur_type, const char* cbor_hex); \ No newline at end of file +const char* parse_kaspa_pskt(struct ExternError*, const char* ur_type, const char* cbor_hex); diff --git a/libs/ur-registry-ffi/README.md b/libs/ur-registry-ffi/README.md index 6609a67..be8adfc 100644 --- a/libs/ur-registry-ffi/README.md +++ b/libs/ur-registry-ffi/README.md @@ -24,6 +24,28 @@ ur-registry-ffi = "0.0.1" - This crate builds as `cdylib` and `staticlib` - Default feature includes `jni` +## PSBT Extend + +The iOS C exports are declared in `include/URRegistryFFI/lib_ur_registry_ffi.h`: + +- `generate_crypto_psbt_extend(err, psbt_hex, coin_id)` returns + `{"type":"crypto-psbt-extend","cbor":"..."}`. +- `parse_crypto_psbt_extend(err, ur_type, cbor_hex)` returns + `{"psbt":"...","coin_id":2}`. An omitted coin field returns `coin_id: null`. + +Android wrappers in `com.keystone.sdk.KeystoneNativeSDK` can declare: + +```java +public static native String generateCryptoPSBTExtend(String psbtHex, int coinId); +public static native String parseCryptoPSBTExtend(String urType, String cborHex); +``` + +Use coin ID `0` for Bitcoin (the CBOR coin field is omitted), `2` for Litecoin, +`3` for Dogecoin, `4` for Dash, or `145` for Bitcoin Cash. Other IDs are rejected. +Hex inputs may include a `0x` prefix. Errors are returned as JSON with an `error` +field, following the existing PSBT interface. These helpers wrap PSBT bytes; +they do not validate or sign the underlying transaction. + ## Related Crates - [`ur-registry`](../ur-registry/README.md) diff --git a/libs/ur-registry-ffi/src/bitcoin/crypto_psbt_extend.rs b/libs/ur-registry-ffi/src/bitcoin/crypto_psbt_extend.rs new file mode 100644 index 0000000..fc15eaa --- /dev/null +++ b/libs/ur-registry-ffi/src/bitcoin/crypto_psbt_extend.rs @@ -0,0 +1,111 @@ +use anyhow::{Error, format_err}; +use serde_json::json; +use ur_registry::crypto_psbt_extend::{CryptoPSBTExtend, SupportedPsbtCoin}; +use ur_registry::registry_types::CRYPTO_PSBT_EXTEND; +use ur_registry::traits::{From, To}; + +use crate::export; +use crate::util_internal::string_helper::remove_prefix_0x; + +export! { + @Java_com_keystone_sdk_KeystoneNativeSDK_generateCryptoPSBTExtend + fn generate_crypto_psbt_extend(psbt_hex: &str, coin_id: i32) -> String { + let coin_id = if coin_id == 0 { None } else { Some(i128::from(coin_id)) }; + if coin_id.is_some_and(|id| !SupportedPsbtCoin::is_valid_coin_id(id)) { + return json!({"error": "invalid coin_id"}).to_string(); + } + let encode = || -> Result, Error> { + let psbt = hex::decode(remove_prefix_0x(psbt_hex))?; + let crypto = CryptoPSBTExtend::new(psbt, coin_id)?; + Ok(crypto.to_bytes()?) + }; + match encode() { + Ok(cbor) => json!({"type": CRYPTO_PSBT_EXTEND.get_type(), "cbor": hex::encode(cbor)}).to_string(), + Err(_) => json!({"error": "PSBT is invalid"}).to_string(), + } + } + + @Java_com_keystone_sdk_KeystoneNativeSDK_parseCryptoPSBTExtend + fn parse_crypto_psbt_extend(ur_type: &str, cbor_hex: &str) -> String { + if ur_type != CRYPTO_PSBT_EXTEND.get_type() { + return json!({"error": "type not match"}).to_string(); + } + let parse = || -> Result { + let cbor = hex::decode(remove_prefix_0x(cbor_hex))?; + let crypto = CryptoPSBTExtend::from_cbor(cbor)?; + if crypto.get_coin_id().is_some_and(|id| !SupportedPsbtCoin::is_valid_coin_id(id)) { + return Err(format_err!("invalid coin_id")); + } + Ok(crypto) + }; + match parse() { + Ok(crypto) => json!({"psbt": hex::encode(crypto.get_psbt()), "coin_id": crypto.get_coin_id()}).to_string(), + Err(_) => json!({"error": "PSBT is invalid"}).to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::Value; + + #[test] + fn supported_coins_round_trip() { + for coin_id in [0, 2, 3, 4, 145] { + let generated: Value = + serde_json::from_str(&generate_crypto_psbt_extend("0x70736274ff", coin_id)) + .unwrap(); + assert_eq!(generated["type"], "crypto-psbt-extend"); + let parsed: Value = serde_json::from_str(&parse_crypto_psbt_extend( + "crypto-psbt-extend", + generated["cbor"].as_str().unwrap(), + )) + .unwrap(); + assert_eq!( + parsed, + json!({"psbt": "70736274ff", "coin_id": if coin_id == 0 { None } else { Some(coin_id) }}) + ); + } + } + + #[test] + fn known_cbor_vectors() { + assert_eq!( + generate_crypto_psbt_extend("aabb", 2), + json!({"type": "crypto-psbt-extend", "cbor": "a20142aabb0202"}).to_string() + ); + assert_eq!( + generate_crypto_psbt_extend("aabb", 0), + json!({"type": "crypto-psbt-extend", "cbor": "a10142aabb"}).to_string() + ); + assert_eq!( + parse_crypto_psbt_extend("crypto-psbt-extend", "0xA20142AABB021891"), + json!({"psbt": "aabb", "coin_id": 145}).to_string() + ); + } + + #[test] + fn invalid_inputs() { + for coin_id in [-1, 1, 5, i32::MAX] { + assert_eq!( + generate_crypto_psbt_extend("aabb", coin_id), + json!({"error": "invalid coin_id"}).to_string() + ); + } + assert_eq!( + generate_crypto_psbt_extend("707", 2), + json!({"error": "PSBT is invalid"}).to_string() + ); + assert_eq!( + parse_crypto_psbt_extend("crypto-psbt", "a10142aabb"), + json!({"error": "type not match"}).to_string() + ); + for cbor in ["zz", "a201", "42aabb", "a20142aabb0201"] { + assert_eq!( + parse_crypto_psbt_extend("crypto-psbt-extend", cbor), + json!({"error": "PSBT is invalid"}).to_string() + ); + } + } +} diff --git a/libs/ur-registry-ffi/src/bitcoin/mod.rs b/libs/ur-registry-ffi/src/bitcoin/mod.rs index 5692c03..7c3cbbc 100644 --- a/libs/ur-registry-ffi/src/bitcoin/mod.rs +++ b/libs/ur-registry-ffi/src/bitcoin/mod.rs @@ -1,3 +1,4 @@ pub mod btc_sign_request; pub mod btc_signature; pub mod crypto_psbt; +pub mod crypto_psbt_extend;