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
5 changes: 4 additions & 1 deletion include/URRegistryFFI/lib_ur_registry_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
const char* parse_kaspa_pskt(struct ExternError*, const char* ur_type, const char* cbor_hex);
22 changes: 22 additions & 0 deletions libs/ur-registry-ffi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
111 changes: 111 additions & 0 deletions libs/ur-registry-ffi/src/bitcoin/crypto_psbt_extend.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>, 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<CryptoPSBTExtend, Error> {
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()
);
}
}
}
1 change: 1 addition & 0 deletions libs/ur-registry-ffi/src/bitcoin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod btc_sign_request;
pub mod btc_signature;
pub mod crypto_psbt;
pub mod crypto_psbt_extend;
Loading