Skip to content

Commit a649c99

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add float-to-int conversion support for WASM
Enable nontrapping float-to-int conversions in wasm-opt to support alternative coin address derivation. Added TryFromJsValue trait for cleaner JS object parsing. Supported since ~2020 in major browsers and runtimes. Issue: BTC-2650 Co-authored-by: llm-git <[email protected]>
1 parent ad2a991 commit a649c99

File tree

5 files changed

+91
-107
lines changed

5 files changed

+91
-107
lines changed

packages/wasm-utxo/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ endef
1212

1313
# run wasm-opt separately so we can pass `--enable-bulk-memory`
1414
define WASM_OPT_COMMAND
15-
$(WASM_OPT) --enable-bulk-memory -Oz $(1)/*.wasm -o $(1)/*.wasm
15+
$(WASM_OPT) --enable-bulk-memory --enable-nontrapping-float-to-int -Oz $(1)/*.wasm -o $(1)/*.wasm
1616
endef
1717

1818
define REMOVE_GITIGNORE

packages/wasm-utxo/src/address/utxolib_compat.rs

Lines changed: 6 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
/// but for now we need to keep this compatibility layer.
44
use wasm_bindgen::JsValue;
55

6-
use crate::address::{bech32, cashaddr, AddressError, Base58CheckCodec};
6+
use crate::address::{bech32, cashaddr, Base58CheckCodec};
77
use crate::bitcoin::{Script, ScriptBuf};
88

9+
pub use crate::address::AddressError;
10+
911
type Result<T> = std::result::Result<T, AddressError>;
1012

1113
pub struct CashAddr {
@@ -24,111 +26,9 @@ pub struct Network {
2426
impl Network {
2527
/// Parse a Network object from a JavaScript value
2628
pub fn from_js_value(js_network: &JsValue) -> Result<Self> {
27-
// Helper to get a required number field
28-
let get_number = |key: &str| -> Result<u32> {
29-
let value =
30-
js_sys::Reflect::get(js_network, &JsValue::from_str(key)).map_err(|_| {
31-
AddressError::InvalidAddress(format!(
32-
"Failed to read {} from network object",
33-
key
34-
))
35-
})?;
36-
37-
value
38-
.as_f64()
39-
.ok_or_else(|| AddressError::InvalidAddress(format!("{} must be a number", key)))
40-
.map(|n| n as u32)
41-
};
42-
43-
// Helper to get an optional string field
44-
let get_optional_string = |key: &str| -> Result<Option<String>> {
45-
let value =
46-
js_sys::Reflect::get(js_network, &JsValue::from_str(key)).map_err(|_| {
47-
AddressError::InvalidAddress(format!(
48-
"Failed to read {} from network object",
49-
key
50-
))
51-
})?;
52-
53-
if value.is_undefined() || value.is_null() {
54-
Ok(None)
55-
} else {
56-
value
57-
.as_string()
58-
.ok_or_else(|| {
59-
AddressError::InvalidAddress(format!("{} must be a string", key))
60-
})
61-
.map(Some)
62-
}
63-
};
64-
65-
let pub_key_hash = get_number("pubKeyHash")?;
66-
let script_hash = get_number("scriptHash")?;
67-
let bech32 = get_optional_string("bech32")?;
68-
69-
// Parse optional cashAddr object
70-
let cash_addr = {
71-
let cash_addr_obj = js_sys::Reflect::get(js_network, &JsValue::from_str("cashAddr"))
72-
.map_err(|_| {
73-
AddressError::InvalidAddress(
74-
"Failed to read cashAddr from network object".to_string(),
75-
)
76-
})?;
77-
78-
if cash_addr_obj.is_undefined() || cash_addr_obj.is_null() {
79-
None
80-
} else {
81-
let prefix = js_sys::Reflect::get(&cash_addr_obj, &JsValue::from_str("prefix"))
82-
.map_err(|_| {
83-
AddressError::InvalidAddress("Failed to read cashAddr.prefix".to_string())
84-
})?
85-
.as_string()
86-
.ok_or_else(|| {
87-
AddressError::InvalidAddress("cashAddr.prefix must be a string".to_string())
88-
})?;
89-
90-
let pub_key_hash =
91-
js_sys::Reflect::get(&cash_addr_obj, &JsValue::from_str("pubKeyHash"))
92-
.map_err(|_| {
93-
AddressError::InvalidAddress(
94-
"Failed to read cashAddr.pubKeyHash".to_string(),
95-
)
96-
})?
97-
.as_f64()
98-
.ok_or_else(|| {
99-
AddressError::InvalidAddress(
100-
"cashAddr.pubKeyHash must be a number".to_string(),
101-
)
102-
})? as u32;
103-
104-
let script_hash =
105-
js_sys::Reflect::get(&cash_addr_obj, &JsValue::from_str("scriptHash"))
106-
.map_err(|_| {
107-
AddressError::InvalidAddress(
108-
"Failed to read cashAddr.scriptHash".to_string(),
109-
)
110-
})?
111-
.as_f64()
112-
.ok_or_else(|| {
113-
AddressError::InvalidAddress(
114-
"cashAddr.scriptHash must be a number".to_string(),
115-
)
116-
})? as u32;
117-
118-
Some(CashAddr {
119-
prefix,
120-
pub_key_hash,
121-
script_hash,
122-
})
123-
}
124-
};
125-
126-
Ok(Network {
127-
pub_key_hash,
128-
script_hash,
129-
cash_addr,
130-
bech32,
131-
})
29+
use crate::try_from_js_value::TryFromJsValue;
30+
Network::try_from_js_value(js_network)
31+
.map_err(|e| AddressError::InvalidAddress(e.to_string()))
13232
}
13333
}
13434

packages/wasm-utxo/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,9 @@ impl WasmMiniscriptError {
4343
WasmMiniscriptError::StringError(s.to_string())
4444
}
4545
}
46+
47+
impl From<crate::address::AddressError> for WasmMiniscriptError {
48+
fn from(err: crate::address::AddressError) -> Self {
49+
WasmMiniscriptError::StringError(err.to_string())
50+
}
51+
}

packages/wasm-utxo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod error;
44
mod fixed_script_wallet;
55
mod miniscript;
66
mod psbt;
7+
mod try_from_js_value;
78
mod try_into_js_value;
89

910
// re-export bitcoin from the miniscript crate
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::address::utxolib_compat::{CashAddr, Network};
2+
use crate::error::WasmMiniscriptError;
3+
use wasm_bindgen::JsValue;
4+
5+
pub(crate) trait TryFromJsValue {
6+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError>
7+
where
8+
Self: Sized;
9+
}
10+
11+
// Implement TryFromJsValue for primitive types
12+
13+
impl TryFromJsValue for String {
14+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError> {
15+
value
16+
.as_string()
17+
.ok_or_else(|| WasmMiniscriptError::new("Expected a string"))
18+
}
19+
}
20+
21+
impl TryFromJsValue for u32 {
22+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError> {
23+
value
24+
.as_f64()
25+
.ok_or_else(|| WasmMiniscriptError::new("Expected a number"))
26+
.map(|n| n as u32)
27+
}
28+
}
29+
30+
impl<T: TryFromJsValue> TryFromJsValue for Option<T> {
31+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError> {
32+
if value.is_undefined() || value.is_null() {
33+
Ok(None)
34+
} else {
35+
T::try_from_js_value(value).map(Some)
36+
}
37+
}
38+
}
39+
40+
// Helper function to get a field from an object and convert it using TryFromJsValue
41+
fn get_field<T: TryFromJsValue>(obj: &JsValue, key: &str) -> Result<T, WasmMiniscriptError> {
42+
let field_value = js_sys::Reflect::get(obj, &JsValue::from_str(key))
43+
.map_err(|_| WasmMiniscriptError::new(&format!("Failed to read {} from object", key)))?;
44+
45+
T::try_from_js_value(&field_value)
46+
.map_err(|e| WasmMiniscriptError::new(&format!("{} (field: {})", e, key)))
47+
}
48+
49+
impl TryFromJsValue for Network {
50+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError> {
51+
let pub_key_hash = get_field(value, "pubKeyHash")?;
52+
let script_hash = get_field(value, "scriptHash")?;
53+
let bech32 = get_field(value, "bech32")?;
54+
let cash_addr = get_field(value, "cashAddr")?;
55+
56+
Ok(Network {
57+
pub_key_hash,
58+
script_hash,
59+
cash_addr,
60+
bech32,
61+
})
62+
}
63+
}
64+
65+
impl TryFromJsValue for CashAddr {
66+
fn try_from_js_value(value: &JsValue) -> Result<Self, WasmMiniscriptError> {
67+
let prefix = get_field(value, "prefix")?;
68+
let pub_key_hash = get_field(value, "pubKeyHash")?;
69+
let script_hash = get_field(value, "scriptHash")?;
70+
71+
Ok(CashAddr {
72+
prefix,
73+
pub_key_hash,
74+
script_hash,
75+
})
76+
}
77+
}

0 commit comments

Comments
 (0)