@exodus/bitcoin-api 4.0.2 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.0.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.0.3...@exodus/bitcoin-api@4.0.4) (2025-09-16)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: mixed input transactions incorrectly classified as receive (#6472)
13
+
14
+
15
+
16
+ ## [4.0.3](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.0.2...@exodus/bitcoin-api@4.0.3) (2025-09-05)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: update bip32 to ^4.1.0 (#6406)
23
+
24
+
25
+
6
26
  ## [4.0.2](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.0.1...@exodus/bitcoin-api@4.0.2) (2025-09-01)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.0.2",
3
+ "version": "4.0.4",
4
4
  "description": "Bitcoin transaction and fee monitors, RPC with the blockchain node, other networking code.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@exodus/asset-lib": "^5.0.0",
24
24
  "@exodus/basic-utils": "^3.0.1",
25
- "@exodus/bip32": "^3.3.0",
25
+ "@exodus/bip32": "^4.1.0",
26
26
  "@exodus/bip322-js": "^2.1.0",
27
27
  "@exodus/bip44-constants": "^195.0.0",
28
28
  "@exodus/bitcoin-lib": "^2.4.3",
@@ -59,5 +59,5 @@
59
59
  "type": "git",
60
60
  "url": "git+https://github.com/ExodusMovement/assets.git"
61
61
  },
62
- "gitHead": "afae6f33fd023d0acd33ee536c4bbad98147312d"
62
+ "gitHead": "1e736fa10b97d17bfa72bdbac09b57dc64b34d44"
63
63
  }
@@ -5,7 +5,7 @@ export const signMessage = async ({ privateKey, message: _message }) => {
5
5
  assert(_message?.bip322Message, `expected bip322Message`)
6
6
 
7
7
  const { address, message } = _message.bip322Message
8
- const signedMessage = Signer.sign(Buffer.from(privateKey), address, message)
8
+ const signedMessage = await Signer.signAsync(Buffer.from(privateKey), address, message)
9
9
 
10
10
  return Buffer.isBuffer(signedMessage) ? signedMessage : Buffer.from(signedMessage, 'base64')
11
11
  }
@@ -451,6 +451,9 @@ export class BitcoinMonitorScanner {
451
451
 
452
452
  // if txItem.vin has an address that matches ours, means we've spent this tx
453
453
  let isSent = false
454
+ // Track whether all non-coinbase inputs belong to our wallet
455
+ let allInputsAreOurs = true
456
+
454
457
  txItem.vin.forEach((vin) => {
455
458
  // It's an coinbase vin
456
459
  if (Object.keys(vin).length === 0) return
@@ -459,7 +462,12 @@ export class BitcoinMonitorScanner {
459
462
  from.push(vin.addr)
460
463
 
461
464
  const address = addrMap[vin.addr]
462
- if (!address) return
465
+ if (!address) {
466
+ // This input doesn't belong to our wallet
467
+ allInputsAreOurs = false
468
+ return
469
+ }
470
+
463
471
  // it came from us...
464
472
  txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
465
473
  isSent = true
@@ -597,12 +605,16 @@ export class BitcoinMonitorScanner {
597
605
  if (isSelfSent) {
598
606
  txLogItem.selfSend = true
599
607
  txLogItem.coinAmount = currency.ZERO
600
- } else {
608
+ } else if (allInputsAreOurs) {
601
609
  // we want coinAmount to be without the fee
602
- // so far coinAmount = -vins + vouts (to us) = - (sent amount + fees)
610
+ // so far coinAmount = -vins (from us) + vouts (to us) = - (sent amount + fees)
603
611
  // add fees to get only sent amount
612
+ // Only do this adjustment when ALL inputs belong to our wallet to avoid
613
+ // false positives in mixed-input transactions (collaborative transactions like coinjoin)
604
614
  txLogItem.coinAmount = txLogItem.coinAmount.add(txLogItem.feeAmount)
605
615
  }
616
+ // If not all inputs are ours, leave coinAmount as-is to avoid misclassification
617
+ // coinAmount = -vins (from us) + vouts (to us), which will be negative for send transactions
606
618
  } else {
607
619
  txLogItem.from = from
608
620
  }
@@ -1,9 +1,9 @@
1
1
  import { getOwnProperty, memoize } from '@exodus/basic-utils'
2
- import { ECPair } from '@exodus/bitcoinjs'
3
2
  import { privateKeyToPublicKey } from '@exodus/crypto/secp256k1'
4
3
  import KeyIdentifier from '@exodus/key-identifier'
5
4
  import BipPath from 'bip32-path'
6
5
  import assert from 'minimalistic-assert'
6
+ import WIF from 'wif'
7
7
 
8
8
  export const createGetKeyWithMetadata = ({
9
9
  signer,
@@ -32,7 +32,7 @@ export const createGetKeyWithMetadata = ({
32
32
  return getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address)
33
33
  }
34
34
 
35
- return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, networkInfo, purpose, address)
35
+ return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, purpose, address)
36
36
  },
37
37
  ({ address, derivationPath }) => address + '_' + derivationPath
38
38
  )
@@ -41,30 +41,22 @@ function standardGetPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpos
41
41
  const privateWif = getOwnProperty(privateKeysAddressMap, address, 'string')
42
42
  assert(privateWif, `there is no private key for address ${address}`)
43
43
 
44
- // ECPair.fromWIF() rejects network objects whose pubKeyHash/scriptHash are wider than one byte (UInt8).
45
- // so we skip the network argument in that case and let the library infer it from the WIF prefix.
46
- const useNet =
47
- networkInfo && networkInfo.pubKeyHash <= 0xff && networkInfo.scriptHash <= 0xff
48
- ? networkInfo
49
- : undefined
50
-
51
- const { privateKey, compressed } = useNet
52
- ? ECPair.fromWIF(privateWif, useNet)
53
- : ECPair.fromWIF(privateWif)
44
+ // networkInfo is coininfo('{something}').toBitcoinJS()
45
+ const useNet = networkInfo || { wif: 0x80 } // default to bitcoin
46
+ const { privateKey, compressed, version } = WIF.decode(privateWif)
47
+ if (version !== useNet.wif) throw new Error('Invalid network version')
54
48
 
55
49
  const publicKey = privateKeyToPublicKey({ privateKey, compressed, format: 'buffer' })
56
- return { privateKey, publicKey, purpose }
50
+ return { privateKey: Buffer.from(privateKey), publicKey, purpose }
57
51
  }
58
52
 
59
- function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, networkInfo, purpose, address) {
53
+ function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, purpose, address) {
60
54
  const path = getOwnProperty(addressPathsMap, address, 'string')
61
55
  assert(hdkeys, 'hdkeys must be provided')
62
56
  assert(purpose, `purpose for address ${address} could not be resolved`)
63
57
  const hdkey = hdkeys[purpose]
64
58
  assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
65
- const derivedhdkey = hdkey.derive(path)
66
- const { privateKey } = ECPair.fromPrivateKey(derivedhdkey.privateKey, { network: networkInfo })
67
- const publicKey = derivedhdkey.publicKey
59
+ const { privateKey, publicKey } = hdkey.derive(path)
68
60
  return { privateKey, publicKey, purpose }
69
61
  }
70
62