Skip to content

Commit dffbd6c

Browse files
authored
feat: amount and address parsing errors (MetaMask#73)
1 parent 5e307aa commit dffbd6c

File tree

5 files changed

+156
-43
lines changed

5 files changed

+156
-43
lines changed

src/types/address.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ use bdk_wallet::{
44
bitcoin::{Address as BdkAddress, AddressType as BdkAddressType, Network as BdkNetwork, ScriptBuf as BdkScriptBuf},
55
AddressInfo as BdkAddressInfo,
66
};
7+
use bitcoin::address::ParseError;
78
use wasm_bindgen::prelude::wasm_bindgen;
89

9-
use crate::result::JsResult;
10+
use crate::{
11+
result::JsResult,
12+
types::{BdkError, BdkErrorCode},
13+
};
1014

1115
use super::{KeychainKind, Network};
1216

@@ -75,7 +79,7 @@ impl Deref for Address {
7579

7680
#[wasm_bindgen]
7781
impl Address {
78-
pub fn from_string(address_str: &str, network: Network) -> JsResult<Self> {
82+
pub fn from_string(address_str: &str, network: Network) -> Result<Self, BdkError> {
7983
let address = BdkAddress::from_str(address_str)?.require_network(network.into())?;
8084
Ok(Address(address))
8185
}
@@ -106,6 +110,24 @@ impl From<Address> for BdkAddress {
106110
}
107111
}
108112

113+
impl From<ParseError> for BdkError {
114+
fn from(e: ParseError) -> Self {
115+
use ParseError::*;
116+
match &e {
117+
Base58(_) => BdkError::new(BdkErrorCode::Base58, e.to_string(), ()),
118+
Bech32(_) => BdkError::new(BdkErrorCode::Bech32, e.to_string(), ()),
119+
WitnessVersion(_) => BdkError::new(BdkErrorCode::WitnessVersion, e.to_string(), ()),
120+
WitnessProgram(_) => BdkError::new(BdkErrorCode::WitnessProgram, e.to_string(), ()),
121+
UnknownHrp(_) => BdkError::new(BdkErrorCode::UnknownHrp, e.to_string(), ()),
122+
LegacyAddressTooLong(_) => BdkError::new(BdkErrorCode::LegacyAddressTooLong, e.to_string(), ()),
123+
InvalidBase58PayloadLength(_) => BdkError::new(BdkErrorCode::InvalidBase58PayloadLength, e.to_string(), ()),
124+
InvalidLegacyPrefix(_) => BdkError::new(BdkErrorCode::InvalidLegacyPrefix, e.to_string(), ()),
125+
NetworkValidation(_) => BdkError::new(BdkErrorCode::NetworkValidation, e.to_string(), ()),
126+
_ => BdkError::new(BdkErrorCode::Unexpected, e.to_string(), ()),
127+
}
128+
}
129+
}
130+
109131
/// An owned, growable script.
110132
///
111133
/// `ScriptBuf` is the most common script type that has the ownership over the contents of the

src/types/amount.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::ops::Deref;
22

33
use bdk_wallet::bitcoin::{Amount as BdkAmount, Denomination as BdkDenomination};
4+
use bitcoin::amount::ParseAmountError;
45
use serde::Serialize;
56
use wasm_bindgen::prelude::wasm_bindgen;
67

7-
use crate::result::JsResult;
8+
use crate::types::{BdkError, BdkErrorCode};
89

910
/// Amount
1011
///
@@ -16,7 +17,7 @@ pub struct Amount(BdkAmount);
1617

1718
#[wasm_bindgen]
1819
impl Amount {
19-
pub fn from_btc(btc: f64) -> JsResult<Self> {
20+
pub fn from_btc(btc: f64) -> Result<Self, BdkError> {
2021
let amount = BdkAmount::from_btc(btc)?;
2122
Ok(Amount(amount))
2223
}
@@ -65,6 +66,20 @@ impl From<Amount> for BdkAmount {
6566
}
6667
}
6768

69+
impl From<ParseAmountError> for BdkError {
70+
fn from(e: ParseAmountError) -> Self {
71+
use ParseAmountError::*;
72+
match &e {
73+
OutOfRange(_) => BdkError::new(BdkErrorCode::OutOfRange, e.to_string(), ()),
74+
TooPrecise(_) => BdkError::new(BdkErrorCode::TooPrecise, e.to_string(), ()),
75+
MissingDigits(_) => BdkError::new(BdkErrorCode::MissingDigits, e.to_string(), ()),
76+
InputTooLarge(_) => BdkError::new(BdkErrorCode::InputTooLarge, e.to_string(), ()),
77+
InvalidCharacter(_) => BdkError::new(BdkErrorCode::InvalidCharacter, e.to_string(), ()),
78+
_ => BdkError::new(BdkErrorCode::Unexpected, e.to_string(), ()),
79+
}
80+
}
81+
}
82+
6883
#[wasm_bindgen]
6984
pub struct SentAndReceived(pub Amount, pub Amount);
7085

src/types/error.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ impl BdkError {
4343
#[wasm_bindgen]
4444
#[derive(Clone, Copy)]
4545
pub enum BdkErrorCode {
46+
/// ------- Transaction creation errors -------
47+
4648
/// There was a problem with the descriptors passed in
4749
Descriptor,
4850
/// There was a problem while extracting and manipulating policies
@@ -83,4 +85,42 @@ pub enum BdkErrorCode {
8385
MissingNonWitnessUtxo,
8486
/// Miniscript PSBT error
8587
MiniscriptPsbt,
88+
89+
/// ------- Address errors -------
90+
91+
/// Base58 error.
92+
Base58,
93+
/// Bech32 segwit decoding error.
94+
Bech32,
95+
/// A witness version conversion/parsing error.
96+
WitnessVersion,
97+
/// A witness program error.
98+
WitnessProgram,
99+
/// Tried to parse an unknown HRP.
100+
UnknownHrp,
101+
/// Legacy address is too long.
102+
LegacyAddressTooLong,
103+
/// Invalid base58 payload data length for legacy address.
104+
InvalidBase58PayloadLength,
105+
/// Invalid legacy address prefix in base58 data payload.
106+
InvalidLegacyPrefix,
107+
/// Address's network differs from required one.
108+
NetworkValidation,
109+
110+
/// ------- Amount errors -------
111+
112+
/// The amount is too big or too small.
113+
OutOfRange,
114+
/// Amount has higher precision than supported by the type.
115+
TooPrecise,
116+
/// A digit was expected but not found.
117+
MissingDigits,
118+
/// Input string was too large.
119+
InputTooLarge,
120+
/// Invalid character in input.
121+
InvalidCharacter,
122+
123+
/// ------- Other errors -------
124+
/// Unexpected error, should never happen
125+
Unexpected,
86126
}

tests/node/integration/esplora.test.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {
88
UnconfirmedTx,
99
Wallet,
1010
SignOptions,
11-
BdkError,
12-
BdkErrorCode,
1311
} from "../../../pkg/bitcoindevkit";
1412

1513
// Tests are expected to run in order
@@ -61,7 +59,7 @@ describe("Esplora client", () => {
6159
feeRate = new FeeRate(BigInt(Math.floor(fee)));
6260
});
6361

64-
it.skip("sends a transaction", async () => {
62+
it("sends a transaction", async () => {
6563
const sendAmount = Amount.from_sat(BigInt(1000));
6664
expect(wallet.balance.trusted_spendable.to_sat()).toBeGreaterThan(
6765
sendAmount.to_sat()
@@ -107,39 +105,4 @@ describe("Esplora client", () => {
107105
.finish();
108106
}).toThrow();
109107
});
110-
111-
it("catches fine-grained errors and deserializes its data", () => {
112-
// Amount should be too big so we fail with InsufficientFunds
113-
const sendAmount = Amount.from_sat(BigInt(2000000000));
114-
115-
try {
116-
wallet
117-
.build_tx()
118-
.fee_rate(new FeeRate(BigInt(1)))
119-
.add_recipient(new Recipient(recipientAddress, sendAmount))
120-
.finish();
121-
} catch (error) {
122-
expect(error).toBeInstanceOf(BdkError);
123-
124-
const { code, message, data } = error;
125-
expect(code).toBe(BdkErrorCode.InsufficientFunds);
126-
expect(message.startsWith("Insufficient funds:")).toBe(true);
127-
expect(data.needed).toBe(2000000000 + 110);
128-
expect(data.available).toBeDefined();
129-
}
130-
131-
try {
132-
wallet
133-
.build_tx()
134-
.fee_rate(new FeeRate(BigInt(1)))
135-
.finish();
136-
} catch (error) {
137-
expect(error).toBeInstanceOf(BdkError);
138-
139-
const { code, message, data } = error;
140-
expect(code).toBe(BdkErrorCode.NoRecipients);
141-
expect(message).toBe("Cannot build tx without recipients");
142-
expect(data).toBeUndefined();
143-
}
144-
});
145108
});

tests/node/integration/wallet.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { Wallet } from "../../../pkg/bitcoindevkit";
1+
import {
2+
Address,
3+
Amount,
4+
BdkError,
5+
BdkErrorCode,
6+
FeeRate,
7+
Recipient,
8+
Wallet,
9+
} from "../../../pkg/bitcoindevkit";
210
import type { Network } from "../../../pkg/bitcoindevkit";
311

412
describe("Wallet", () => {
@@ -8,6 +16,10 @@ describe("Wallet", () => {
816
const internalDesc =
917
"wpkh(tprv8ZgxMBicQKsPf6vydw7ixvsLKY79hmeXujBkGCNCApyft92yVYng2y28JpFZcneBYTTHycWSRpokhHE25GfHPBxnW5GpSm2dMWzEi9xxEyU/84'/1'/0'/1/*)#dd6w3a4e";
1018
let wallet: Wallet;
19+
const recipientAddress = Address.from_string(
20+
"tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v",
21+
network
22+
);
1123

1224
it("creates a new wallet from descriptors", () => {
1325
wallet = Wallet.create(network, externalDesc, internalDesc);
@@ -42,4 +54,65 @@ describe("Wallet", () => {
4254
loadedWallet.next_unused_address("external").address.toString()
4355
).toBe("tb1qjtgffm20l9vu6a7gacxvpu2ej4kdcsgc26xfdz");
4456
});
57+
58+
it("catches fine-grained errors and deserializes its data", () => {
59+
// Amount should be too big so we fail with InsufficientFunds
60+
const sendAmount = Amount.from_sat(BigInt(2000000000));
61+
62+
try {
63+
wallet
64+
.build_tx()
65+
.fee_rate(new FeeRate(BigInt(1)))
66+
.add_recipient(new Recipient(recipientAddress, sendAmount))
67+
.finish();
68+
} catch (error) {
69+
expect(error).toBeInstanceOf(BdkError);
70+
71+
const { code, message, data } = error;
72+
expect(code).toBe(BdkErrorCode.InsufficientFunds);
73+
expect(message.startsWith("Insufficient funds:")).toBe(true);
74+
expect(data.needed).toBe(2000000000 + 42);
75+
expect(data.available).toBeDefined();
76+
}
77+
});
78+
79+
it("catches fine-grained address errors", () => {
80+
try {
81+
Address.from_string(
82+
"tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v",
83+
"bitcoin"
84+
);
85+
} catch (error) {
86+
expect(error).toBeInstanceOf(BdkError);
87+
88+
const { code, message, data } = error;
89+
expect(code).toBe(BdkErrorCode.NetworkValidation);
90+
expect(message.startsWith("validation error")).toBe(true);
91+
expect(data).toBeUndefined();
92+
}
93+
94+
try {
95+
Address.from_string("notAnAddress", network);
96+
} catch (error) {
97+
expect(error).toBeInstanceOf(BdkError);
98+
99+
const { code, message, data } = error;
100+
expect(code).toBe(BdkErrorCode.Base58);
101+
expect(message.startsWith("base58 error")).toBe(true);
102+
expect(data).toBeUndefined();
103+
}
104+
});
105+
106+
it("catches fine-grained amount errors", () => {
107+
try {
108+
Amount.from_btc(-100000000);
109+
} catch (error) {
110+
expect(error).toBeInstanceOf(BdkError);
111+
112+
const { code, message, data } = error;
113+
expect(code).toBe(BdkErrorCode.OutOfRange);
114+
expect(message.startsWith("amount out of range")).toBe(true);
115+
expect(data).toBeUndefined();
116+
}
117+
});
45118
});

0 commit comments

Comments
 (0)