diff --git a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt index 4c3dd99..35d5fcd 100644 --- a/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt +++ b/bdk-android/lib/src/androidTest/kotlin/org/bitcoindevkit/OfflineWalletTest.kt @@ -30,6 +30,11 @@ class OfflineWalletTest { ) val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) + assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") + assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") + assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") + assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") + assertEquals( expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", actual = addressInfo.address.asString() diff --git a/bdk-ffi/src/bdk.udl b/bdk-ffi/src/bdk.udl index b90f059..e86b465 100644 --- a/bdk-ffi/src/bdk.udl +++ b/bdk-ffi/src/bdk.udl @@ -315,6 +315,8 @@ interface Address { string to_qr_uri(); string as_string(); + + boolean is_valid_for_network(Network network); }; interface Transaction { diff --git a/bdk-ffi/src/bitcoin.rs b/bdk-ffi/src/bitcoin.rs index effa01a..f95fa27 100644 --- a/bdk-ffi/src/bitcoin.rs +++ b/bdk-ffi/src/bitcoin.rs @@ -34,6 +34,7 @@ impl From for Script { } } +#[derive(PartialEq, Debug)] pub enum Network { Bitcoin, Testnet, @@ -121,6 +122,15 @@ impl Address { pub fn as_string(&self) -> String { self.inner.to_string() } + + pub fn is_valid_for_network(&self, network: Network) -> bool { + let address_str = self.inner.to_string(); + if let Ok(unchecked_address) = address_str.parse::>() { + unchecked_address.is_valid_for_network(network.into()) + } else { + false + } + } } impl From
for BdkAddress { @@ -314,3 +324,354 @@ impl From<&BdkTxOut> for TxOut { } } } + +#[cfg(test)] +mod tests { + use crate::bitcoin::Address; + use crate::bitcoin::Network; + + #[test] + fn test_is_valid_for_network() { + // ====Docs tests==== + // https://docs.rs/bitcoin/0.29.2/src/bitcoin/util/address.rs.html#798-802 + + let docs_address_testnet_str = "2N83imGV3gPwBzKJQvWJ7cRUY2SpUyU6A5e"; + let docs_address_testnet = + Address::new(docs_address_testnet_str.to_string(), Network::Testnet).unwrap(); + assert!( + docs_address_testnet.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + docs_address_testnet.is_valid_for_network(Network::Signet), + "Address should be valid for Signet" + ); + assert!( + docs_address_testnet.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + assert_ne!( + docs_address_testnet.network(), + Network::Bitcoin, + "Address should not be parsed as Bitcoin" + ); + + let docs_address_mainnet_str = "32iVBEu4dxkUQk9dJbZUiBiQdmypcEyJRf"; + let docs_address_mainnet = + Address::new(docs_address_mainnet_str.to_string(), Network::Bitcoin).unwrap(); + assert!( + docs_address_mainnet.is_valid_for_network(Network::Bitcoin), + "Address should be valid for Bitcoin" + ); + assert_ne!( + docs_address_mainnet.network(), + Network::Testnet, + "Address should not be valid for Testnet" + ); + assert_ne!( + docs_address_mainnet.network(), + Network::Signet, + "Address should not be valid for Signet" + ); + assert_ne!( + docs_address_mainnet.network(), + Network::Regtest, + "Address should not be valid for Regtest" + ); + + // ====Bech32==== + + // | Network | Prefix | Address Type | + // |-----------------|---------|--------------| + // | Bitcoin Mainnet | `bc1` | Bech32 | + // | Bitcoin Testnet | `tb1` | Bech32 | + // | Bitcoin Signet | `tb1` | Bech32 | + // | Bitcoin Regtest | `bcrt1` | Bech32 | + + // Bech32 - Bitcoin + // Valid for: + // - Bitcoin + // Not valid for: + // - Testnet + // - Signet + // - Regtest + let bitcoin_mainnet_bech32_address_str = "bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj"; + let bitcoin_mainnet_bech32_address = Address::new( + bitcoin_mainnet_bech32_address_str.to_string(), + Network::Bitcoin, + ) + .unwrap(); + assert!( + bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Bitcoin), + "Address should be valid for Bitcoin" + ); + assert!( + !bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Testnet), + "Address should not be valid for Testnet" + ); + assert!( + !bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Signet), + "Address should not be valid for Signet" + ); + assert!( + !bitcoin_mainnet_bech32_address.is_valid_for_network(Network::Regtest), + "Address should not be valid for Regtest" + ); + + // Bech32 - Testnet + // Valid for: + // - Testnet + // - Regtest + // Not valid for: + // - Bitcoin + // - Regtest + let bitcoin_testnet_bech32_address_str = + "tb1p4nel7wkc34raczk8c4jwk5cf9d47u2284rxn98rsjrs4w3p2sheqvjmfdh"; + let bitcoin_testnet_bech32_address = Address::new( + bitcoin_testnet_bech32_address_str.to_string(), + Network::Testnet, + ) + .unwrap(); + assert!( + !bitcoin_testnet_bech32_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_testnet_bech32_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_testnet_bech32_address.is_valid_for_network(Network::Signet), + "Address should be valid for Signet" + ); + assert!( + !bitcoin_testnet_bech32_address.is_valid_for_network(Network::Regtest), + "Address should not not be valid for Regtest" + ); + + // Bech32 - Signet + // Valid for: + // - Signet + // - Testnet + // Not valid for: + // - Bitcoin + // - Regtest + let bitcoin_signet_bech32_address_str = + "tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk"; + let bitcoin_signet_bech32_address = Address::new( + bitcoin_signet_bech32_address_str.to_string(), + Network::Signet, + ) + .unwrap(); + assert!( + !bitcoin_signet_bech32_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_signet_bech32_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_signet_bech32_address.is_valid_for_network(Network::Signet), + "Address should be valid for Signet" + ); + assert!( + !bitcoin_signet_bech32_address.is_valid_for_network(Network::Regtest), + "Address should not not be valid for Regtest" + ); + + // Bech32 - Regtest + // Valid for: + // - Regtest + // Not valid for: + // - Bitcoin + // - Testnet + // - Signet + let bitcoin_regtest_bech32_address_str = "bcrt1q39c0vrwpgfjkhasu5mfke9wnym45nydfwaeems"; + let bitcoin_regtest_bech32_address = Address::new( + bitcoin_regtest_bech32_address_str.to_string(), + Network::Regtest, + ) + .unwrap(); + assert!( + !bitcoin_regtest_bech32_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + !bitcoin_regtest_bech32_address.is_valid_for_network(Network::Testnet), + "Address should not be valid for Testnet" + ); + assert!( + !bitcoin_regtest_bech32_address.is_valid_for_network(Network::Signet), + "Address should not be valid for Signet" + ); + assert!( + bitcoin_regtest_bech32_address.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + + // ====P2PKH==== + + // | Network | Prefix for P2PKH | Prefix for P2SH | + // |------------------------------------|------------------|-----------------| + // | Bitcoin Mainnet | `1` | `3` | + // | Bitcoin Testnet, Regtest, Signet | `m` or `n` | `2` | + + // P2PKH - Bitcoin + // Valid for: + // - Bitcoin + // Not valid for: + // - Testnet + // - Regtest + let bitcoin_mainnet_p2pkh_address_str = "1FfmbHfnpaZjKFvyi1okTjJJusN455paPH"; + let bitcoin_mainnet_p2pkh_address = Address::new( + bitcoin_mainnet_p2pkh_address_str.to_string(), + Network::Bitcoin, + ) + .unwrap(); + assert!( + bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Bitcoin), + "Address should be valid for Bitcoin" + ); + assert!( + !bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Testnet), + "Address should not be valid for Testnet" + ); + assert!( + !bitcoin_mainnet_p2pkh_address.is_valid_for_network(Network::Regtest), + "Address should not be valid for Regtest" + ); + + // P2PKH - Testnet + // Valid for: + // - Testnet + // - Regtest + // Not valid for: + // - Bitcoin + let bitcoin_testnet_p2pkh_address_str = "mucFNhKMYoBQYUAEsrFVscQ1YaFQPekBpg"; + let bitcoin_testnet_p2pkh_address = Address::new( + bitcoin_testnet_p2pkh_address_str.to_string(), + Network::Testnet, + ) + .unwrap(); + assert!( + !bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_testnet_p2pkh_address.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + + // P2PKH - Regtest + // Valid for: + // - Testnet + // - Regtest + // Not valid for: + // - Bitcoin + let bitcoin_regtest_p2pkh_address_str = "msiGFK1PjCk8E6FXeoGkQPTscmcpyBdkgS"; + let bitcoin_regtest_p2pkh_address = Address::new( + bitcoin_regtest_p2pkh_address_str.to_string(), + Network::Regtest, + ) + .unwrap(); + assert!( + !bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_regtest_p2pkh_address.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + + // ====P2SH==== + + // | Network | Prefix for P2PKH | Prefix for P2SH | + // |------------------------------------|------------------|-----------------| + // | Bitcoin Mainnet | `1` | `3` | + // | Bitcoin Testnet, Regtest, Signet | `m` or `n` | `2` | + + // P2SH - Bitcoin + // Valid for: + // - Bitcoin + // Not valid for: + // - Testnet + // - Regtest + let bitcoin_mainnet_p2sh_address_str = "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"; + let bitcoin_mainnet_p2sh_address = Address::new( + bitcoin_mainnet_p2sh_address_str.to_string(), + Network::Bitcoin, + ) + .unwrap(); + assert!( + bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Bitcoin), + "Address should be valid for Bitcoin" + ); + assert!( + !bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Testnet), + "Address should not be valid for Testnet" + ); + assert!( + !bitcoin_mainnet_p2sh_address.is_valid_for_network(Network::Regtest), + "Address should not be valid for Regtest" + ); + + // P2SH - Testnet + // Valid for: + // - Testnet + // - Regtest + // Not valid for: + // - Bitcoin + let bitcoin_testnet_p2sh_address_str = "2NFUBBRcTJbYc1D4HSCbJhKZp6YCV4PQFpQ"; + let bitcoin_testnet_p2sh_address = Address::new( + bitcoin_testnet_p2sh_address_str.to_string(), + Network::Testnet, + ) + .unwrap(); + assert!( + !bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_testnet_p2sh_address.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + + // P2SH - Regtest + // Valid for: + // - Testnet + // - Regtest + // Not valid for: + // - Bitcoin + let bitcoin_regtest_p2sh_address_str = "2NEb8N5B9jhPUCBchz16BB7bkJk8VCZQjf3"; + let bitcoin_regtest_p2sh_address = Address::new( + bitcoin_regtest_p2sh_address_str.to_string(), + Network::Regtest, + ) + .unwrap(); + assert!( + !bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Bitcoin), + "Address should not be valid for Bitcoin" + ); + assert!( + bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Testnet), + "Address should be valid for Testnet" + ); + assert!( + bitcoin_regtest_p2sh_address.is_valid_for_network(Network::Regtest), + "Address should be valid for Regtest" + ); + } +} diff --git a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt index 8574473..d990c51 100644 --- a/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt +++ b/bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/OfflineWalletTest.kt @@ -3,6 +3,7 @@ package org.bitcoindevkit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue +import kotlin.test.assertFalse class OfflineWalletTest { @Test @@ -27,6 +28,11 @@ class OfflineWalletTest { ) val addressInfo: AddressInfo = wallet.getAddress(AddressIndex.New) + assertTrue(addressInfo.address.isValidForNetwork(Network.TESTNET), "Address is not valid for testnet network") + assertTrue(addressInfo.address.isValidForNetwork(Network.SIGNET), "Address is not valid for signet network") + assertFalse(addressInfo.address.isValidForNetwork(Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") + assertFalse(addressInfo.address.isValidForNetwork(Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") + assertEquals( expected = "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", actual = addressInfo.address.asString() diff --git a/bdk-python/tests/test_offline_wallet.py b/bdk-python/tests/test_offline_wallet.py index 4fc6ccd..2b35e4b 100644 --- a/bdk-python/tests/test_offline_wallet.py +++ b/bdk-python/tests/test_offline_wallet.py @@ -15,6 +15,11 @@ class TestSimpleWallet(unittest.TestCase): ) address_info: bdk.AddressInfo = wallet.get_address(bdk.AddressIndex.NEW()) + self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.TESTNET), "Address is not valid for testnet network") + self.assertTrue(address_info.address.is_valid_for_network(bdk.Network.SIGNET), "Address is not valid for signet network") + self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.REGTEST), "Address is valid for regtest network, but it shouldn't be") + self.assertFalse(address_info.address.is_valid_for_network(bdk.Network.BITCOIN), "Address is valid for bitcoin network, but it shouldn't be") + self.assertEqual("tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e", address_info.address.as_string()) def test_balance(self): diff --git a/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift b/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift index 273fc86..f19051b 100644 --- a/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift +++ b/bdk-swift/Tests/BitcoinDevKitTests/OfflineWalletTests.swift @@ -13,7 +13,16 @@ final class OfflineWalletTests: XCTestCase { network: .testnet ) let addressInfo: AddressInfo = wallet.getAddress(addressIndex: AddressIndex.new) - + + XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.testnet), + "Address is not valid for testnet network") + XCTAssertTrue(addressInfo.address.isValidForNetwork(network: Network.signet), + "Address is not valid for signet network") + XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.regtest), + "Address is valid for regtest network, but it shouldn't be") + XCTAssertFalse(addressInfo.address.isValidForNetwork(network: Network.bitcoin), + "Address is valid for bitcoin network, but it shouldn't be") + XCTAssertEqual(addressInfo.address.asString(), "tb1qzg4mckdh50nwdm9hkzq06528rsu73hjxxzem3e") }