Repository: input-output-hk/lace
Pull request #1823: test: add unit tests for bitcoin common module
Author: login: AngelCastilloB
Created: 2025-04-15T11:14:25Z | Merged: 2025-04-22T08:52:48Z

# Checklist

- [ ] JIRA - \<[link](https://input-output.atlassian.net/browse/LW-12559)>
- [ ] Proper tests implemented
- [ ] Screenshots added.

---

## Proposed solution

This PR adds unit tests to the common module of the Bitcoin package. Progressing towards completion of https://input-output.atlassian.net/browse/LW-12559



Diff:
diff --git a/packages/bitcoin/package.json b/packages/bitcoin/package.json
index 51cb071258..57afc73fe7 100644
--- a/packages/bitcoin/package.json
+++ b/packages/bitcoin/package.json
@@ -34,7 +34,7 @@
     "prepare": "ts-patch install -s",
     "prestart": "yarn build",
     "start": "node dist/index.js",
-    "test": "echo \"@lace/bitcoin: no test command specified\"",
+    "test": "NODE_ENV=test run -T jest -c ./test/jest.config.js --silent",
     "tsc:declarationOnly": "tsc --project ./src/tsconfig.declarationOnly.json",
     "type-check": "echo \"@lace/bitcoin: no type-check command specified\"",
     "watch": "yarn build --watch"
diff --git a/packages/bitcoin/src/wallet/README.md b/packages/bitcoin/src/wallet/README.md
index ec87dacd96..e4fc517861 100644
--- a/packages/bitcoin/src/wallet/README.md
+++ b/packages/bitcoin/src/wallet/README.md
@@ -1,4 +1,33 @@
-# Light Wallet | Packages | Cardano | Wallet
+# **Bitcoin Observable Wallet Proof of Concept**
 
-A thin wrapper around the `cardano-js-sdk` to define a blockchain wallet module conforming to
-a basic interface exposed by the core package.
+This project is a implementation of a simple Bitcoin wallet, built using **TypeScript** and leveraging:
+- **BitcoinJS** for address/key derivation and transaction building/signing.
+- **Maestro API** to interact with the Bitcoin network (Testnet or Mainnet) for transaction history, UTXO retrieval, and broadcasting transactions.
+- **RxJS** for reactive programming to observe wallet state, transaction history, and UTXO updates.
+
+---
+
+## **Features**
+
+1. **Address and Key Derivation**:
+  - Supports **BIP32** and **Electrum-compatible** derivation paths.
+  - Generates:
+    - Legacy (P2PKH)
+    - SegWit (P2SH-P2WPKH)
+    - Native SegWit (P2WPKH)
+    - Taproot (P2TR) addresses.
+
+2. **Transaction History and UTXOs**:
+  - Fetches transaction history for specified addresses.
+  - Retrieves **UTXOs** (Unspent Transaction Outputs) for address spending.
+
+3. **Balance Tracking**:
+  - Continuously tracks and updates wallet balance based on UTXOs.
+
+4. **Transaction Building and Signing**:
+  - Constructs a raw transaction from available UTXOs.
+  - Signs the transaction using the derived private key.
+  - Submits the transaction to the Bitcoin network.
+
+5. **Transaction Tracking**:
+  - Tracks the status of a transaction (e.g., Pending, Confirmed, or Dropped) until it is included in a block.
diff --git a/packages/bitcoin/src/wallet/types.ts b/packages/bitcoin/src/wallet/types.ts
deleted file mode 100644
index d75564ccd6..0000000000
--- a/packages/bitcoin/src/wallet/types.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { Cardano, Paginated } from '@cardano-sdk/core';
-import type { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
-import { WalletType } from '@cardano-sdk/web-extension';
-
-export type LedgerConnection = LedgerKeyAgent['deviceConnection'];
-
-export type DeviceConnection = LedgerConnection | boolean;
-
-export type HardwareWallets = WalletType.Trezor | WalletType.Ledger;
-
-export type HardwareWalletConnection =
-  | {
-      type: WalletType.Trezor;
-    }
-  | {
-      type: WalletType.Ledger;
-      value: LedgerConnection;
-    };
-
-export type StakePoolSearchResults = Paginated<Cardano.StakePool>;
-
-export type DappInfo = {
-  name: string;
-  logo: string;
-  url: string;
-};
-
-export type CoinId = {
-  id: string;
-  symbol: string;
-  name: string;
-  decimals: number;
-};
-
-export enum TransactionStatus {
-  SUCCESS = 'success',
-  PENDING = 'sending',
-  ERROR = 'error',
-  SPENDABLE = 'spendable',
-  AWAITING_COSIGNATURES = 'awaiting_cosignatures'
-}
-
-export enum Cip30TxType {
-  Send = 'Send',
-  Mint = 'Mint',
-  Burn = 'Burn',
-  DRepRegistration = 'DRepRegistration',
-  DRepRetirement = 'DRepRetirement',
-  DRepUpdate = 'DRepUpdate',
-  VoteDelegation = 'VoteDelegation',
-  VotingProcedures = 'VotingProcedures',
-  VoteRegistrationDelegation = 'VoteRegistrationDelegation',
-  StakeRegistrationDelegation = 'StakeRegistrationDelegation',
-  StakeVoteDelegationRegistration = 'StakeVoteDelegationRegistration',
-  StakeVoteDelegation = 'StakeVoteDelegation',
-  ProposalProcedures = 'ProposalProcedures'
-}
-
-export type Cip30SignTxOutput = {
-  coins: string;
-  recipient: string;
-  assets?: Cip30SignTxAssetItem[];
-};
-
-export type Cip30SignTxSummary = {
-  fee: string;
-  outputs: Cip30SignTxOutput[];
-  type: Cip30TxType;
-  mintedAssets?: Cip30SignTxAssetItem[];
-  burnedAssets?: Cip30SignTxAssetItem[];
-  collateral?: string;
-};
-
-export type Cip30SignTxAssetItem = {
-  name: string;
-  amount: string;
-  ticker?: string;
-};
-export enum WalletManagerProviderTypes {
-  CARDANO_SERVICES_PROVIDER = 'cardano-services-provider'
-}
-
-export type ChainName = keyof typeof Cardano.ChainIds;
diff --git a/packages/bitcoin/test/.eslintrc.js b/packages/bitcoin/test/.eslintrc.js
deleted file mode 100644
index d9645d72b4..0000000000
--- a/packages/bitcoin/test/.eslintrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
-  extends: ['../../../test/.eslintrc.js'],
-  parserOptions: {
-    project: './tsconfig.json',
-    tsconfigRootDir: __dirname
-  }
-};
diff --git a/packages/bitcoin/test/__mocks__/fileMock.js b/packages/bitcoin/test/__mocks__/fileMock.js
deleted file mode 100644
index 86059f3629..0000000000
--- a/packages/bitcoin/test/__mocks__/fileMock.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = 'test-file-stub';
diff --git a/packages/bitcoin/test/__mocks__/styleMock.js b/packages/bitcoin/test/__mocks__/styleMock.js
deleted file mode 100644
index f053ebf797..0000000000
--- a/packages/bitcoin/test/__mocks__/styleMock.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = {};
diff --git a/packages/bitcoin/test/__mocks__/svgMock.js b/packages/bitcoin/test/__mocks__/svgMock.js
deleted file mode 100644
index 0b5014d292..0000000000
--- a/packages/bitcoin/test/__mocks__/svgMock.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = { ReactComponent: 'div' };
diff --git a/packages/bitcoin/test/address.test.ts b/packages/bitcoin/test/address.test.ts
new file mode 100644
index 0000000000..dbc38b4512
--- /dev/null
+++ b/packages/bitcoin/test/address.test.ts
@@ -0,0 +1,160 @@
+/* eslint-disable no-magic-numbers, no-loop-func */
+import * as bitcoin from 'bitcoinjs-lib';
+import * as ecc from '@bitcoinerlab/secp256k1';
+import { Network } from '../src/wallet/lib/common/network';
+import {
+  AddressType,
+  deriveAddressByType,
+  validateBitcoinAddress,
+  AddressValidationResult,
+  isP2trAddress
+} from '../src/wallet/lib/common/address';
+
+bitcoin.initEccLib(ecc);
+
+const networkMap: Record<string, bitcoin.Network> = {
+  mainnet: bitcoin.networks.bitcoin,
+  testnet: bitcoin.networks.testnet
+};
+
+const addressTypeMap: Record<string, AddressType> = {
+  p2pkh: AddressType.Legacy,
+  'p2sh-p2wpkh': AddressType.SegWit,
+  p2wpkh: AddressType.NativeSegWit,
+  p2tr: AddressType.Taproot
+};
+
+type Vector = {
+  privateKey?: string;
+  publicKey: string;
+  address: string;
+  network: string;
+  format: string;
+};
+
+// Test vectors taken from https://github.com/hirosystems/stacks-blockchain-api/blob/ae0eb65c4d6901db172f2b4ea751817d8ef8c05c/src/tests/helpers-tests.ts#L193-L530
+const TEST_VECTORS: Vector[] = [
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
+    network: 'mainnet',
+    format: 'p2pkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN',
+    network: 'mainnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
+    network: 'mainnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9',
+    network: 'mainnet',
+    format: 'p2tr'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: '2NEb2fNbJXdwi7EC6vKCjWUTA12PABNniQM',
+    network: 'testnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1qzepy04hjksj6c4m3ggawdjqvw48hzu4swvwmvt',
+    network: 'testnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv',
+    network: 'testnet',
+    format: 'p2tr'
+  }
+];
+
+describe('Addresses', () => {
+  describe('Bitcoin address derivation (from test vectors)', () => {
+    for (const vector of TEST_VECTORS) {
+      const { publicKey, address, network, format } = vector;
+
+      const testName = `${format.toUpperCase()} | ${network} | ${publicKey.slice(0, 10)}...`;
+      const addressType = addressTypeMap[format];
+
+      it(`produces correct address: ${testName}`, () => {
+        const derived = deriveAddressByType(Buffer.from(publicKey, 'hex'), addressType, networkMap[network]);
+        expect(derived).toBe(address);
+      });
+    }
+  });
+
+  describe('validateBitcoinAddress', () => {
+    it('validates a correct legacy address (Mainnet)', () => {
+      expect(validateBitcoinAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Mainnet)', () => {
+      expect(validateBitcoinAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Mainnet)', () => {
+      expect(
+        validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Mainnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid testnet address on mainnet', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Mainnet)
+      ).toBe(AddressValidationResult.InvalidNetwork);
+    });
+
+    it('validates a correct legacy address (Testnet)', () => {
+      expect(validateBitcoinAddress('mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Testnet)', () => {
+      expect(validateBitcoinAddress('tb1q597d0yvt3mg3k9p5qtkz8lh3j53nsssr57wnfr', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Testnet)', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Testnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid mainnet address on testnet', () => {
+      expect(
+        validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Testnet)
+      ).toBe(AddressValidationResult.InvalidNetwork);
+    });
+
+    it('returns InvalidAddress for malformed address', () => {
+      expect(validateBitcoinAddress('notARealAddress', Network.Mainnet)).toBe(AddressValidationResult.InvalidAddress);
+    });
+  });
+
+  describe('isP2trAddress', () => {
+    it('identifies P2TR address correctly', () => {
+      expect(isP2trAddress('bc1pmyrcn4jl9x8gtz8wjyqchghzq39kech5xg99snm4a3p2l3thf4esfp20h9')).toBe(true);
+      expect(isP2trAddress('tb1pmyrcn4jl9x8gtz8wjyqchghzq39kech5xg99snm4a3p2l3thf4esfp20h9')).toBe(true);
+    });
+
+    it('rejects non-taproot address', () => {
+      expect(isP2trAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq')).toBe(false);
+    });
+  });
+});
diff --git a/packages/bitcoin/test/info.test.ts b/packages/bitcoin/test/info.test.ts
new file mode 100644
index 0000000000..7b33c657bb
--- /dev/null
+++ b/packages/bitcoin/test/info.test.ts
@@ -0,0 +1,43 @@
+import { getNetworkKeys, BitcoinWalletInfo, ExtendedAccountPublicKeys } from '../src/wallet/lib/common/info';
+import { Network } from '../src/wallet/lib/common/network';
+
+describe('getNetworkKeys', () => {
+  const mainnetKeys: ExtendedAccountPublicKeys = {
+    legacy: 'xpub-mainnet-legacy',
+    segWit: 'xpub-mainnet-segwit',
+    nativeSegWit: 'xpub-mainnet-nativesegwit',
+    taproot: 'xpub-mainnet-taproot',
+    electrumNativeSegWit: 'xpub-mainnet-electrum'
+  };
+
+  const testnetKeys: ExtendedAccountPublicKeys = {
+    legacy: 'tpub-testnet-legacy',
+    segWit: 'tpub-testnet-segwit',
+    nativeSegWit: 'tpub-testnet-nativesegwit',
+    taproot: 'tpub-testnet-taproot',
+    electrumNativeSegWit: 'tpub-testnet-electrum'
+  };
+
+  const walletInfo: BitcoinWalletInfo = {
+    walletName: 'Test Wallet',
+    accountIndex: 0,
+    encryptedSecrets: {
+      mnemonics: 'encrypted-mnemonic-placeholder',
+      seed: 'encrypted-seed-placeholder'
+    },
+    extendedAccountPublicKeys: {
+      mainnet: mainnetKeys,
+      testnet: testnetKeys
+    }
+  };
+
+  it('returns mainnet keys for Network.Mainnet', () => {
+    const keys = getNetworkKeys(walletInfo, Network.Mainnet);
+    expect(keys).toEqual(mainnetKeys);
+  });
+
+  it('returns testnet keys for Network.Testnet', () => {
+    const keys = getNetworkKeys(walletInfo, Network.Testnet);
+    expect(keys).toEqual(testnetKeys);
+  });
+});
diff --git a/packages/bitcoin/test/jest.config.js b/packages/bitcoin/test/jest.config.js
index ca53ca5496..34bd354c42 100644
--- a/packages/bitcoin/test/jest.config.js
+++ b/packages/bitcoin/test/jest.config.js
@@ -1,26 +1,13 @@
-const { pathsToModuleNameMapper } = require('ts-jest');
-const { compilerOptions } = require('../src/tsconfig');
-const { createJestConfig } = require('../../../test/createJestConfig');
-
-const rootDir = process.cwd();
-
-module.exports = createJestConfig({
-  moduleNameMapper: {
-    '.*\\.(scss|sass|css|less)$': '<rootDir>/test/__mocks__/styleMock.js',
-    '.*\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)$': '<rootDir>/test/__mocks__/fileMock.js',
-    '^[.]*(?!.*\\.component\\.svg$).*\\.svg*$': '<rootDir>/test/__mocks__/fileMock.js',
-    'component\\.svg(\\?v=\\d+\\.d+\\.\\d+)?$': '<rootDir>/test/__mocks__/svgMock.js',
-    '^lodash-es$': 'lodash',
-    '^webextension-polyfill': '<rootDir>/test/__mocks__/fileMock.js',
-    // https://github.com/LedgerHQ/ledger-live/issues/763
-    '@ledgerhq/devices/hid-framing': '@ledgerhq/devices/lib/hid-framing',
-    // TODO update uuid and retest; https://github.com/uuidjs/uuid/issues/451
-    uuid: require.resolve('uuid'),
-    ...pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src' })
-  },
-  roots: ['<rootDir>/src'],
-  testTimeout: 60000,
-  testEnvironment: 'jsdom',
-  setupFilesAfterEnv: ['./test/jest.setup.js', 'jest-canvas-mock'],
-  workerThreads: true
-});
+module.exports = {
+  preset: 'ts-jest/presets/default',
+  testEnvironment: 'node',
+  testMatch: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
+  transform: {
+    '^.+\\.tsx?$': [
+      'ts-jest',
+      {
+        tsconfig: './test/tsconfig.json'
+      }
+    ]
+  }
+};
diff --git a/packages/bitcoin/test/jest.setup.js b/packages/bitcoin/test/jest.setup.js
deleted file mode 100644
index c789ce94b4..0000000000
--- a/packages/bitcoin/test/jest.setup.js
+++ /dev/null
@@ -1,18 +0,0 @@
-Object.defineProperty(window, 'matchMedia', {
-  writable: true,
-  value: jest.fn().mockImplementation((query) => ({
-    matches: false,
-    media: query,
-    onchange: null,
-    addListener: jest.fn(), // deprecated
-    removeListener: jest.fn(), // deprecated
-    addEventListener: jest.fn(),
-    removeEventListener: jest.fn(),
-    dispatchEvent: jest.fn()
-  }))
-});
-
-// Add Uint8Array to prototype chain of Buffer, so that it behaves the same in jsdom as in nodejs and polyfilled browser env
-let Type = Buffer;
-while (Type.prototype) Type = Type.prototype;
-Object.setPrototypeOf(Type, Uint8Array.prototype);
diff --git a/packages/bitcoin/test/keyDerivation.test.ts b/packages/bitcoin/test/keyDerivation.test.ts
new file mode 100644
index 0000000000..e7fd61c2a5
--- /dev/null
+++ b/packages/bitcoin/test/keyDerivation.test.ts
@@ -0,0 +1,188 @@
+import {
+  deriveBip39Seed,
+  deriveElectrumSeed,
+  ChainType,
+  deriveAccountRootKeyPair,
+  deriveChildPublicKey,
+  deriveChildKeyPair,
+  getExtendedPubKeys
+} from '../src/wallet/lib/common/keyDerivation';
+import { AddressType } from '../src/wallet/lib/common/address';
+import { Network } from '../src/wallet/lib/common/network';
+
+// taken from https://github.com/trezor/python-mnemonic/blob/master/vectors.json
+const trezorBip39vectors = [
+  [
+    'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
+    'c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04'
+  ],
+  [
+    'legal winner thank year wave sausage worth useful legal winner thank yellow',
+    '2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607'
+  ],
+  [
+    'letter advice cage absurd amount doctor acoustic avoid letter advice cage above',
+    'd71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8'
+  ],
+  [
+    'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong',
+    'ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069'
+  ],
+  [
+    'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent',
+    '035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa'
+  ],
+  [
+    'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will',
+    'f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd'
+  ],
+  [
+    'letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always',
+    '107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65'
+  ],
+  [
+    'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when',
+    '0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528'
+  ],
+  [
+    'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
+    'bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8'
+  ],
+  [
+    'legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title',
+    'bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87'
+  ],
+  [
+    'letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless',
+    'c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f'
+  ],
+  [
+    'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote',
+    'dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad'
+  ],
+  [
+    'ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic',
+    '274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028'
+  ],
+  [
+    'gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog',
+    '628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac'
+  ],
+  [
+    'hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length',
+    '64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440'
+  ],
+  [
+    'scheme spot photo card baby mountain device kick cradle pact join borrow',
+    'ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612'
+  ],
+  [
+    'horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave',
+    'fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d'
+  ],
+  [
+    'panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside',
+    '72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d'
+  ],
+  [
+    'cat swing flag economy stadium alone churn speed unique patch report train',
+    'deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5'
+  ],
+  [
+    'light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access',
+    '4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02'
+  ],
+  [
+    'all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform',
+    '26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d'
+  ],
+  [
+    'vessel ladder alter error federal sibling chat ability sun glass valve picture',
+    '2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f'
+  ],
+  [
+    'scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump',
+    '7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88'
+  ],
+  [
+    'void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold',
+    '01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998'
+  ]
+];
+
+describe('seed derivation', () => {
+  it('deriveBip39Seed', () => {
+    trezorBip39vectors.forEach(([mnemonic, expectedSeedHex]) => {
+      const seed = deriveBip39Seed(mnemonic, 'TREZOR');
+      expect(seed.toString('hex')).toBe(expectedSeedHex);
+    });
+  });
+
+  it('deriveElectrumSeed', () => {
+    const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
+    const expectedSeed =
+      'b24af40d049942cb7e7ea70ce919f5e665782a751ee51fb372bb13b76b1bbca307bc4c12dca4baa037e7cfe915ff7bc02167abe97a5bdc2766f8878c54c59fc4';
+
+    const seed = deriveElectrumSeed(mnemonic, '');
+    expect(seed.toString('hex')).toBe(expectedSeed);
+  });
+});
+
+// Vectors for key derivation generated with https://iancoleman.io/bip39/
+describe('Key derivation', () => {
+  it('deriveAccountRootKeyPair', () => {
+    const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
+    const seed = deriveBip39Seed(mnemonic);
+    const { pair, path } = deriveAccountRootKeyPair(seed, AddressType.Legacy, Network.Mainnet, 0);
+    expect(path).toBe("m/44'/0'/0'");
+    expect(pair.privateKey).toBe(
+      'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb'
+    );
+    expect(pair.publicKey).toBe(
+      'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj'
+    );
+  });
+
+  it('deriveChildKeyPair', () => {
+    const xpriv =
+      'xprv9xpXFhFpqdQK3TmytPBqXtGSwS3DLjojFhTGht8gwAAii8py5X6pxeBnQ6ehJiyJ6nDjWGJfZ95WxByFXVkDxHXrqu53WCRGypk2ttuqncb';
+    const { pair, path } = deriveChildKeyPair(xpriv, ChainType.External, 0);
+    expect(path).toBe('m/0/0');
+
+    expect(pair.privateKey.toString('hex')).toBe('e284129cc0922579a535bbf4d1a3b25773090d28c909bc0fed73b5e0222cc372');
+    expect(pair.publicKey.toString('hex')).toBe('03aaeb52dd7494c361049de67cc680e83ebcbbbdbeb13637d92cd845f70308af5e');
+  });
+
+  it('deriveChildPublicKey', () => {
+    const xpub =
+      'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj';
+    const childPubKey = deriveChildPublicKey(xpub, ChainType.External, 0);
+    expect(childPubKey.toString('hex')).toBe('03aaeb52dd7494c361049de67cc680e83ebcbbbdbeb13637d92cd845f70308af5e');
+  });
+});
+
+// Vectors for key derivation generated with https://iancoleman.io/bip39/
+describe('getExtendedPubKeys', () => {
+  const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
+  const seed = deriveBip39Seed(mnemonic);
+  const account = 0;
+
+  it('derives all supported xpubs for mainnet account 0', () => {
+    const keys = getExtendedPubKeys(seed, account);
+    expect(keys.mainnet.legacy).toBe(
+      'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj'
+    );
+    expect(keys.mainnet.nativeSegWit).toBe(
+      'xpub6CatWdiZiodmUeTDp8LT5or8nmbKNcuyvz7WyksVFkKB4RHwCD3XyuvPEbvqAQY3rAPshWcMLoP2fMFMKHPJ4ZeZXYVUhLv1VMrjPC7PW6V'
+    );
+    expect(keys.mainnet.taproot).toBe(
+      'xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ'
+    );
+    expect(keys.mainnet.segWit).toBe(
+      'xpub6C6nQwHaWbSrzs5tZ1q7m5R9cPK9eYpNMFesiXsYrgc1P8bvLLAet9JfHjYXKjToD8cBRswJXXbbFpXgwsswVPAZzKMa1jUp2kVkGVUaJa7'
+    );
+    expect(keys.mainnet.electrumNativeSegWit).toBe(
+      'xpub6BiChRN7aqq51RA7RnAmKhqKdGckPncrHWLrj1xoj6ZMfdMJ1dX4Ysh9V3yEhpFCpC3BapjR83xPKY693XXTEU6qgWU3qZs78WBHA15uhYf'
+    );
+  });
+});
diff --git a/packages/bitcoin/test/taproot.test.ts b/packages/bitcoin/test/taproot.test.ts
new file mode 100644
index 0000000000..6aa9fef609
--- /dev/null
+++ b/packages/bitcoin/test/taproot.test.ts
@@ -0,0 +1,35 @@
+/* eslint-disable no-magic-numbers */
+import { tweakTaprootPubKey, tweakTaprootPrivateKey } from '../src/wallet/lib/common/taproot';
+
+// Test vectors from https://github.com/bitcoin/bips/blob/master/bip-0341/wallet-test-vectors.json
+describe('Taproot key tweaking (BIP341)', () => {
+  it('tweaks public key correctly with no merkle root', () => {
+    const internalPubKey = Buffer.from('d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d', 'hex');
+
+    const tweaked = tweakTaprootPubKey(internalPubKey);
+    const expected = '53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343';
+
+    expect(tweaked.toString('hex')).toBe(expected);
+  });
+
+  it('tweaks private key correctly with internal pubkey', () => {
+    const internalPubKey = Buffer.from('d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d', 'hex');
+    const privateKey = Buffer.from('00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'hex');
+
+    const tweakedPriv = tweakTaprootPrivateKey(privateKey, internalPubKey);
+
+    const expected = 'b91926939e4655dd519d6aeee5666cbb9b9856b8d6fe7fac5e160eee2847171a';
+
+    expect(tweakedPriv.toString('hex')).toBe(expected);
+  });
+
+  it('throws on invalid pubkey length', () => {
+    const badKey = Buffer.alloc(31);
+    expect(() => tweakTaprootPubKey(badKey)).toThrow('Internal public key must be 32 bytes (x-only format).');
+  });
+
+  it('throws if tweak fails (invalid pubkey)', () => {
+    const zeroKey = Buffer.alloc(32, 0);
+    expect(() => tweakTaprootPubKey(zeroKey)).toThrow('Expected Point');
+  });
+});
diff --git a/packages/bitcoin/test/tsconfig.json b/packages/bitcoin/test/tsconfig.json
index 02bc0d6221..312e2a29da 100644
--- a/packages/bitcoin/test/tsconfig.json
+++ b/packages/bitcoin/test/tsconfig.json
@@ -1,12 +1,12 @@
 {
   "extends": "../../../tsconfig.json",
   "compilerOptions": {
-    "allowSyntheticDefaultImports": true,
-    "paths": {
-      "@src/*": ["../src/*"]
-    },
-    "baseUrl": ".",
-    "rootDir": ".."
+    "baseUrl": "."
   },
-  "include": ["../src/*", "../src/ui/typings"]
+  "include": ["./**/*.ts", "../src/**/*.ts"],
+  "references": [
+    {
+      "path": "../src"
+    }
+  ]
 }


Reviews:
user: login: iccicci
body: Apart from a minor change on a test description: excellent!
Moreover it seems the branch needs a rebase due to merge conflict.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T11:25:38Z
user: login: AngelCastilloB
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T11:53:22Z
user: login: iccicci
body: Very nice!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T11:54:59Z
user: login: mkazlauskas
body: Nice work! 🚀 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T05:19:18Z

Review comments:
diff_hunk: @@ -0,0 +1,160 @@
+/* eslint-disable no-magic-numbers, no-loop-func */
+import * as bitcoin from 'bitcoinjs-lib';
+import * as ecc from '@bitcoinerlab/secp256k1';
+import { Network } from '../src/wallet/lib/common/network';
+import {
+  AddressType,
+  deriveAddressByType,
+  validateBitcoinAddress,
+  AddressValidationResult,
+  isP2trAddress
+} from '../src/wallet/lib/common/address';
+
+bitcoin.initEccLib(ecc);
+
+const networkMap: Record<string, bitcoin.Network> = {
+  mainnet: bitcoin.networks.bitcoin,
+  testnet: bitcoin.networks.testnet
+};
+
+const addressTypeMap: Record<string, AddressType> = {
+  p2pkh: AddressType.Legacy,
+  'p2sh-p2wpkh': AddressType.SegWit,
+  p2wpkh: AddressType.NativeSegWit,
+  p2tr: AddressType.Taproot
+};
+
+type Vector = {
+  privateKey?: string;
+  publicKey: string;
+  address: string;
+  network: string;
+  format: string;
+};
+
+// Test vectors taken from https://github.com/hirosystems/stacks-blockchain-api/blob/ae0eb65c4d6901db172f2b4ea751817d8ef8c05c/src/tests/helpers-tests.ts#L193-L530
+const TEST_VECTORS: Vector[] = [
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
+    network: 'mainnet',
+    format: 'p2pkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN',
+    network: 'mainnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
+    network: 'mainnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9',
+    network: 'mainnet',
+    format: 'p2tr'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: '2NEb2fNbJXdwi7EC6vKCjWUTA12PABNniQM',
+    network: 'testnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1qzepy04hjksj6c4m3ggawdjqvw48hzu4swvwmvt',
+    network: 'testnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv',
+    network: 'testnet',
+    format: 'p2tr'
+  }
+];
+
+describe('Addresses', () => {
+  describe('Bitcoin address derivation (from test vectors)', () => {
+    for (const vector of TEST_VECTORS) {
+      const { publicKey, address, network, format } = vector;
+
+      const testName = `${format.toUpperCase()} | ${network} | ${publicKey.slice(0, 10)}...`;
+      const addressType = addressTypeMap[format];
+
+      it(`produces correct address: ${testName}`, () => {
+        const derived = deriveAddressByType(Buffer.from(publicKey, 'hex'), addressType, networkMap[network]);
+        expect(derived).toBe(address);
+      });
+    }
+  });
+
+  describe('validateBitcoinAddress', () => {
+    it('validates a correct legacy address (Mainnet)', () => {
+      expect(validateBitcoinAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Mainnet)', () => {
+      expect(validateBitcoinAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Mainnet)', () => {
+      expect(
+        validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Mainnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid testnet address on mainnet', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Mainnet)
+      ).toBe(AddressValidationResult.InvalidNetwork);
+    });
+
+    it('validates a correct legacy address (Testnet)', () => {
+      expect(validateBitcoinAddress('mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Testnet)', () => {
+      expect(validateBitcoinAddress('tb1q597d0yvt3mg3k9p5qtkz8lh3j53nsssr57wnfr', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Testnet)', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Testnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid testnet address on testnet', () => {
path: packages/bitcoin/test/address.test.ts
user: login: iccicci
body: ```suggestion
    it('returns InvalidNetwork for valid mainnet address on testnet', () => {
```
created_at: 2025-04-18T11:21:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,160 @@
+/* eslint-disable no-magic-numbers, no-loop-func */
+import * as bitcoin from 'bitcoinjs-lib';
+import * as ecc from '@bitcoinerlab/secp256k1';
+import { Network } from '../src/wallet/lib/common/network';
+import {
+  AddressType,
+  deriveAddressByType,
+  validateBitcoinAddress,
+  AddressValidationResult,
+  isP2trAddress
+} from '../src/wallet/lib/common/address';
+
+bitcoin.initEccLib(ecc);
+
+const networkMap: Record<string, bitcoin.Network> = {
+  mainnet: bitcoin.networks.bitcoin,
+  testnet: bitcoin.networks.testnet
+};
+
+const addressTypeMap: Record<string, AddressType> = {
+  p2pkh: AddressType.Legacy,
+  'p2sh-p2wpkh': AddressType.SegWit,
+  p2wpkh: AddressType.NativeSegWit,
+  p2tr: AddressType.Taproot
+};
+
+type Vector = {
+  privateKey?: string;
+  publicKey: string;
+  address: string;
+  network: string;
+  format: string;
+};
+
+// Test vectors taken from https://github.com/hirosystems/stacks-blockchain-api/blob/ae0eb65c4d6901db172f2b4ea751817d8ef8c05c/src/tests/helpers-tests.ts#L193-L530
+const TEST_VECTORS: Vector[] = [
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH',
+    network: 'mainnet',
+    format: 'p2pkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: '3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN',
+    network: 'mainnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
+    network: 'mainnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798',
+    address: 'bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9',
+    network: 'mainnet',
+    format: 'p2tr'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: '2NEb2fNbJXdwi7EC6vKCjWUTA12PABNniQM',
+    network: 'testnet',
+    format: 'p2sh-p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1qzepy04hjksj6c4m3ggawdjqvw48hzu4swvwmvt',
+    network: 'testnet',
+    format: 'p2wpkh'
+  },
+  {
+    publicKey: '03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41',
+    address: 'tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv',
+    network: 'testnet',
+    format: 'p2tr'
+  }
+];
+
+describe('Addresses', () => {
+  describe('Bitcoin address derivation (from test vectors)', () => {
+    for (const vector of TEST_VECTORS) {
+      const { publicKey, address, network, format } = vector;
+
+      const testName = `${format.toUpperCase()} | ${network} | ${publicKey.slice(0, 10)}...`;
+      const addressType = addressTypeMap[format];
+
+      it(`produces correct address: ${testName}`, () => {
+        const derived = deriveAddressByType(Buffer.from(publicKey, 'hex'), addressType, networkMap[network]);
+        expect(derived).toBe(address);
+      });
+    }
+  });
+
+  describe('validateBitcoinAddress', () => {
+    it('validates a correct legacy address (Mainnet)', () => {
+      expect(validateBitcoinAddress('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Mainnet)', () => {
+      expect(validateBitcoinAddress('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', Network.Mainnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Mainnet)', () => {
+      expect(
+        validateBitcoinAddress('bc1pemwrzunpf5tj70s6vkxysf2v8njg60kpc2mvuccath5kfw3zd6jqv9lks9', Network.Mainnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid testnet address on mainnet', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Mainnet)
+      ).toBe(AddressValidationResult.InvalidNetwork);
+    });
+
+    it('validates a correct legacy address (Testnet)', () => {
+      expect(validateBitcoinAddress('mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct native segwit address (Testnet)', () => {
+      expect(validateBitcoinAddress('tb1q597d0yvt3mg3k9p5qtkz8lh3j53nsssr57wnfr', Network.Testnet)).toBe(
+        AddressValidationResult.Valid
+      );
+    });
+
+    it('validates a correct taproot address (Testnet)', () => {
+      expect(
+        validateBitcoinAddress('tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c', Network.Testnet)
+      ).toBe(AddressValidationResult.Valid);
+    });
+
+    it('returns InvalidNetwork for valid testnet address on testnet', () => {
path: packages/bitcoin/test/address.test.ts
user: login: AngelCastilloB
body: done here @iccicci https://github.com/input-output-hk/lace/pull/1823/commits/cd11eb56190b452d36e3020d06247bafce8676e9 , also rebased on main
created_at: 2025-04-18T11:53:22Z
author_association: MEMBER

PR comments:
user: login: lace-bot
created_at: 2025-04-15T11:31:02Z
body: <!-- allure -->
# Allure Report
[`allure-report-publisher`](https://github.com/andrcuns/allure-report-publisher) generated test report!

<!-- jobs -->
<!-- processReports -->
**processReports**: ✅ [test report](https://lace-qa:8PP5wBaDV6UoXj@dq4ajm5i5q7bz.cloudfront.net/smoke/linux/chrome/8920/14534748311/index.html) for [bfbf64b0](https://github.com/input-output-hk/lace/pull/1823/commits/bfbf64b063a93d0dfcbc1d020b9c52c09eceb8be)
|       | passed | failed | skipped | flaky | total | result |
|-------|--------|--------|---------|-------|-------|--------|
| Total | 33     | 0      | 4       | 0     | 37    | ✅     |
<!-- processReports -->
<!-- jobs -->
<!-- allurestop -->
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-18T11:58:37Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=input-output-hk_lace&pullRequest=1823) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=input-output-hk_lace&pullRequest=1823&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=input-output-hk_lace&pullRequest=1823&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=input-output-hk_lace&pullRequest=1823&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=input-output-hk_lace&pullRequest=1823&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=input-output-hk_lace&pullRequest=1823&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=input-output-hk_lace&pullRequest=1823)


author_association: NONE