Skip to content

Commit 9d9fd3a

Browse files
feat(wasm-utxo): expose structured error codes derived from Rust enum variant names
Add a `WasmErrorCode` trait and `impl_wasm_error_code!` macro that derive stable dotted error codes (e.g. `ParseTransactionError.Input/ParseInputError.WalletValidation`) from Rust enum variant names via `strum::IntoStaticStr`. The single `From<WasmUtxoError> for JsValue` bridge now attaches a `code` string property to every thrown `Error` via `Reflect::set`, so callers can dispatch on `err.code` instead of substring-matching `err.message`. New variant names are exposed automatically — zero JS-side boilerplate per error type. Existing `WasmUtxoError::new()` call sites fall back to the `WasmUtxoError.StringError` code unchanged. The TS surface adds an `isWasmUtxoError` type guard and `WasmUtxoError` interface exported from the package root. Refs: T1-3475
1 parent 049be6f commit 9d9fd3a

13 files changed

Lines changed: 261 additions & 20 deletions

File tree

packages/wasm-utxo/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wasm-utxo/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ inspect = ["dep:num-bigint", "dep:serde", "dep:serde_json", "dep:hex"]
2727
[dependencies]
2828
wasm-bindgen = "0.2"
2929
js-sys = "0.3"
30+
strum = { version = "0.27", features = ["derive"] }
3031
miniscript = { git = "https://github.com/BitGo/rust-miniscript", tag = "miniscript-13.0.0-bitgo.5" }
3132
bech32 = "0.11"
3233
musig2 = { version = "0.3.1", default-features = false, features = ["k256"] }

packages/wasm-utxo/js/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ declare module "./wasm/wasm_utxo.js" {
144144
}
145145
}
146146

147+
export interface WasmUtxoError extends Error {
148+
code: string;
149+
}
150+
151+
export function isWasmUtxoError(e: unknown): e is WasmUtxoError {
152+
return e instanceof Error && typeof (e as { code?: unknown }).code === "string";
153+
}
154+
147155
export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js";
148156
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js";
149157
export { Psbt } from "./descriptorWallet/Psbt.js";

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub use networks::{
4949
use crate::bitcoin::{Script, ScriptBuf};
5050
use std::fmt;
5151

52-
#[derive(Debug)]
52+
#[derive(Debug, strum::IntoStaticStr)]
5353
pub enum AddressError {
5454
InvalidScript(String),
5555
InvalidAddress(String),
@@ -75,6 +75,7 @@ impl fmt::Display for AddressError {
7575
}
7676

7777
impl std::error::Error for AddressError {}
78+
crate::impl_wasm_error_code!(AddressError);
7879

7980
type Result<T> = std::result::Result<T, AddressError>;
8081

packages/wasm-utxo/src/error.rs

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,46 @@
11
use core::fmt;
22

3-
#[derive(Debug, Clone)]
3+
use crate::fixed_script_wallet::bitgo_psbt::ParseTransactionError;
4+
5+
pub trait WasmErrorCode {
6+
fn code(&self) -> String;
7+
}
8+
9+
/// Derives `WasmErrorCode` for leaf error enums (no nested error variants).
10+
/// Requires `#[derive(strum::IntoStaticStr)]` on the enum.
11+
#[macro_export]
12+
macro_rules! impl_wasm_error_code {
13+
($t:ty) => {
14+
impl $crate::error::WasmErrorCode for $t {
15+
fn code(&self) -> String {
16+
format!("{}.{}", stringify!($t), <&'static str>::from(self))
17+
}
18+
}
19+
};
20+
}
21+
22+
#[derive(Debug, strum::IntoStaticStr)]
423
pub enum WasmUtxoError {
524
StringError(String),
25+
Parse(ParseTransactionError),
626
}
727

828
impl std::error::Error for WasmUtxoError {}
29+
930
impl fmt::Display for WasmUtxoError {
1031
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1132
match self {
1233
WasmUtxoError::StringError(s) => write!(f, "{}", s),
34+
WasmUtxoError::Parse(e) => write!(f, "{}", e),
35+
}
36+
}
37+
}
38+
39+
impl WasmErrorCode for WasmUtxoError {
40+
fn code(&self) -> String {
41+
match self {
42+
WasmUtxoError::StringError(_) => "WasmUtxoError.StringError".to_string(),
43+
WasmUtxoError::Parse(e) => e.code(),
1344
}
1445
}
1546
}
@@ -38,6 +69,18 @@ impl From<miniscript::descriptor::NonDefiniteKeyError> for WasmUtxoError {
3869
}
3970
}
4071

72+
impl From<crate::address::AddressError> for WasmUtxoError {
73+
fn from(err: crate::address::AddressError) -> Self {
74+
WasmUtxoError::StringError(err.to_string())
75+
}
76+
}
77+
78+
impl From<ParseTransactionError> for WasmUtxoError {
79+
fn from(err: ParseTransactionError) -> Self {
80+
WasmUtxoError::Parse(err)
81+
}
82+
}
83+
4184
impl WasmUtxoError {
4285
pub fn new(s: &str) -> WasmUtxoError {
4386
WasmUtxoError::StringError(s.to_string())
@@ -53,8 +96,77 @@ impl WasmUtxoError {
5396
}
5497
}
5598

56-
impl From<crate::address::AddressError> for WasmUtxoError {
57-
fn from(err: crate::address::AddressError) -> Self {
58-
WasmUtxoError::StringError(err.to_string())
99+
#[cfg(test)]
100+
mod tests {
101+
use super::*;
102+
use crate::fixed_script_wallet::bitgo_psbt::{
103+
psbt_wallet_input::{OutputScriptError, ParseInputError},
104+
psbt_wallet_output::ParseOutputError,
105+
ParseTransactionError,
106+
};
107+
108+
#[test]
109+
fn string_error_code() {
110+
let e = WasmUtxoError::new("oops");
111+
assert_eq!(e.code(), "WasmUtxoError.StringError");
112+
}
113+
114+
#[test]
115+
fn parse_input_wallet_validation_code() {
116+
let inner = ParseInputError::WalletValidation("no script type matches".to_string());
117+
let e = WasmUtxoError::Parse(ParseTransactionError::Input {
118+
index: 0,
119+
error: inner,
120+
});
121+
assert_eq!(
122+
e.code(),
123+
"ParseTransactionError.Input/ParseInputError.WalletValidation"
124+
);
125+
}
126+
127+
#[test]
128+
fn parse_input_utxo_code() {
129+
let inner = ParseInputError::Utxo(OutputScriptError::NoUtxoFields);
130+
let e = WasmUtxoError::Parse(ParseTransactionError::Input {
131+
index: 0,
132+
error: inner,
133+
});
134+
assert_eq!(
135+
e.code(),
136+
"ParseTransactionError.Input/ParseInputError.Utxo/OutputScriptError.NoUtxoFields"
137+
);
138+
}
139+
140+
#[test]
141+
fn parse_output_code() {
142+
let inner = ParseOutputError::WalletMatch("bad".to_string());
143+
let e = WasmUtxoError::Parse(ParseTransactionError::Output {
144+
index: 0,
145+
error: inner,
146+
});
147+
assert_eq!(
148+
e.code(),
149+
"ParseTransactionError.Output/ParseOutputError.WalletMatch"
150+
);
151+
}
152+
153+
#[test]
154+
fn leaf_variants_code() {
155+
assert_eq!(
156+
ParseInputError::ValueOverflow.code(),
157+
"ParseInputError.ValueOverflow"
158+
);
159+
assert_eq!(
160+
ParseInputError::Derivation("x".into()).code(),
161+
"ParseInputError.Derivation"
162+
);
163+
assert_eq!(
164+
ParseInputError::ScriptTypeDetection("x".into()).code(),
165+
"ParseInputError.ScriptTypeDetection"
166+
);
167+
assert_eq!(
168+
OutputScriptError::OutputIndexOutOfBounds { vout: 0 }.code(),
169+
"OutputScriptError.OutputIndexOutOfBounds"
170+
);
59171
}
60172
}

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub use zcash_psbt::{
2727
ZCASH_SAPLING_VERSION_GROUP_ID,
2828
};
2929

30-
#[derive(Debug)]
30+
#[derive(Debug, strum::IntoStaticStr)]
3131
pub enum DeserializeError {
3232
/// Standard bitcoin consensus decoding error
3333
Consensus(miniscript::bitcoin::consensus::encode::Error),
@@ -48,6 +48,7 @@ impl std::fmt::Display for DeserializeError {
4848
}
4949

5050
impl std::error::Error for DeserializeError {}
51+
crate::impl_wasm_error_code!(DeserializeError);
5152

5253
impl From<miniscript::bitcoin::consensus::encode::Error> for DeserializeError {
5354
fn from(e: miniscript::bitcoin::consensus::encode::Error) -> Self {
@@ -61,7 +62,7 @@ impl From<miniscript::bitcoin::psbt::Error> for DeserializeError {
6162
}
6263
}
6364

64-
#[derive(Debug)]
65+
#[derive(Debug, strum::IntoStaticStr)]
6566
pub enum SerializeError {
6667
/// Standard bitcoin consensus encoding error
6768
Consensus(std::io::Error),
@@ -79,6 +80,7 @@ impl std::fmt::Display for SerializeError {
7980
}
8081

8182
impl std::error::Error for SerializeError {}
83+
crate::impl_wasm_error_code!(SerializeError);
8284

8385
impl From<std::io::Error> for SerializeError {
8486
fn from(e: std::io::Error) -> Self {
@@ -136,7 +138,7 @@ pub struct ParsedTransaction {
136138
}
137139

138140
/// Error type for transaction parsing
139-
#[derive(Debug)]
141+
#[derive(Debug, strum::IntoStaticStr)]
140142
pub enum ParseTransactionError {
141143
/// Failed to parse input
142144
Input {
@@ -185,6 +187,21 @@ impl std::fmt::Display for ParseTransactionError {
185187

186188
impl std::error::Error for ParseTransactionError {}
187189

190+
impl crate::error::WasmErrorCode for ParseTransactionError {
191+
fn code(&self) -> String {
192+
let variant: &str = self.into();
193+
match self {
194+
Self::Input { error, .. } => {
195+
format!("ParseTransactionError.{}/{}", variant, error.code())
196+
}
197+
Self::Output { error, .. } => {
198+
format!("ParseTransactionError.{}/{}", variant, error.code())
199+
}
200+
_ => format!("ParseTransactionError.{}", variant),
201+
}
202+
}
203+
}
204+
188205
/// Get the default sighash type for a network and chain type
189206
fn get_default_sighash_type(
190207
network: Network,

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/p2tr_musig2_input.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn derive_xpub_for_input_tap(
7171
}
7272

7373
/// Error types for MuSig2 parsing
74-
#[derive(Debug, Clone, PartialEq, Eq)]
74+
#[derive(Debug, Clone, PartialEq, Eq, strum::IntoStaticStr)]
7575
pub enum Musig2Error {
7676
/// Missing participants
7777
MissingParticipants,
@@ -143,6 +143,7 @@ impl std::fmt::Display for Musig2Error {
143143
}
144144

145145
impl std::error::Error for Musig2Error {}
146+
crate::impl_wasm_error_code!(Musig2Error);
146147

147148
/// MuSig2 participant data
148149
///

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ pub fn parse_shared_chain_and_index(input: &Input) -> Result<(u32, u32), String>
416416
Ok((chain, index))
417417
}
418418

419-
#[derive(Debug)]
419+
#[derive(Debug, strum::IntoStaticStr)]
420420
pub enum OutputScriptError {
421421
OutputIndexOutOfBounds { vout: u32 },
422422
NoUtxoFields,
@@ -436,6 +436,7 @@ impl std::fmt::Display for OutputScriptError {
436436
}
437437

438438
impl std::error::Error for OutputScriptError {}
439+
crate::impl_wasm_error_code!(OutputScriptError);
439440

440441
/// Identifies a key in the wallet triple (user, backup, bitgo)
441442
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -627,7 +628,7 @@ impl ParsedInput {
627628
}
628629

629630
/// Error type for parsing a single PSBT input
630-
#[derive(Debug)]
631+
#[derive(Debug, strum::IntoStaticStr)]
631632
pub enum ParseInputError {
632633
/// Failed to extract output script or value from input
633634
Utxo(OutputScriptError),
@@ -670,6 +671,17 @@ impl std::fmt::Display for ParseInputError {
670671

671672
impl std::error::Error for ParseInputError {}
672673

674+
impl crate::error::WasmErrorCode for ParseInputError {
675+
fn code(&self) -> String {
676+
let variant: &str = self.into();
677+
match self {
678+
Self::Utxo(e) => format!("ParseInputError.{}/{}", variant, e.code()),
679+
Self::Address(e) => format!("ParseInputError.{}/{}", variant, e.code()),
680+
_ => format!("ParseInputError.{}", variant),
681+
}
682+
}
683+
}
684+
673685
/// Get both output script and value from a PSBT input
674686
pub fn get_output_script_and_value(
675687
input: &Input,
@@ -697,7 +709,7 @@ fn get_output_script_from_input(
697709
get_output_script_and_value(input, prevout).map(|(script, _value)| script)
698710
}
699711

700-
#[derive(Debug, Clone, PartialEq, Eq)]
712+
#[derive(Debug, Clone, PartialEq, Eq, strum::IntoStaticStr)]
701713
pub enum InputValidationErrorKind {
702714
/// Failed to extract output script from input
703715
InvalidOutputScript(String),
@@ -739,7 +751,15 @@ impl std::fmt::Display for InputValidationError {
739751
}
740752
}
741753

742-
#[derive(Debug, PartialEq, Eq)]
754+
crate::impl_wasm_error_code!(InputValidationErrorKind);
755+
756+
impl crate::error::WasmErrorCode for InputValidationError {
757+
fn code(&self) -> String {
758+
self.kind.code()
759+
}
760+
}
761+
762+
#[derive(Debug, PartialEq, Eq, strum::IntoStaticStr)]
743763
pub enum PsbtValidationError {
744764
/// Number of prevouts does not match number of PSBT inputs
745765
InputLengthMismatch {
@@ -776,6 +796,19 @@ impl std::fmt::Display for PsbtValidationError {
776796

777797
impl std::error::Error for PsbtValidationError {}
778798

799+
impl crate::error::WasmErrorCode for PsbtValidationError {
800+
fn code(&self) -> String {
801+
let variant: &str = self.into();
802+
match self {
803+
Self::InvalidInputs(errors) => {
804+
let inner = errors.first().map(|e| e.code()).unwrap_or_default();
805+
format!("PsbtValidationError.{}/{}", variant, inner)
806+
}
807+
_ => format!("PsbtValidationError.{}", variant),
808+
}
809+
}
810+
}
811+
779812
/// Validates that all inputs in a PSBT belong to the wallet
780813
pub fn validate_psbt_wallet_inputs(
781814
psbt: &Psbt,

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_output.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl ParsedOutput {
6969
}
7070

7171
/// Error type for parsing a single PSBT output
72-
#[derive(Debug)]
72+
#[derive(Debug, strum::IntoStaticStr)]
7373
pub enum ParseOutputError {
7474
/// Failed to match output to wallet (corruption or validation error)
7575
WalletMatch(String),
@@ -89,3 +89,4 @@ impl std::fmt::Display for ParseOutputError {
8989
}
9090

9191
impl std::error::Error for ParseOutputError {}
92+
crate::impl_wasm_error_code!(ParseOutputError);

packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ impl BitGoPsbt {
987987
let parsed_tx = self
988988
.psbt
989989
.parse_transaction_with_wallet_keys(wallet_keys, replay_protection, &pubkeys)
990-
.map_err(|e| WasmUtxoError::new(&format!("Failed to parse transaction: {}", e)))?;
990+
.map_err(WasmUtxoError::from)?;
991991

992992
// Convert to JsValue directly using TryIntoJsValue
993993
parsed_tx.try_to_js_value()

0 commit comments

Comments
 (0)