@exodus/bitcoin-api 2.31.1 → 2.33.0

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
+ ## [2.33.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.32.0...@exodus/bitcoin-api@2.33.0) (2025-06-30)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: enable zcash buffer signer (#5732)
13
+
14
+
15
+
16
+ ## [2.32.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.31.1...@exodus/bitcoin-api@2.32.0) (2025-04-01)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat(bitcoin): allow message signing with external signer (#5365)
23
+
24
+
25
+
6
26
  ## [2.31.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.31.0...@exodus/bitcoin-api@2.31.1) (2025-03-19)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.31.1",
3
+ "version": "2.33.0",
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",
@@ -23,7 +23,7 @@
23
23
  "@exodus/asset-lib": "^5.0.0",
24
24
  "@exodus/basic-utils": "^3.0.1",
25
25
  "@exodus/bip32": "^3.3.0",
26
- "@exodus/bip322-js": "^2.0.0",
26
+ "@exodus/bip322-js": "^2.1.0",
27
27
  "@exodus/bip44-constants": "^195.0.0",
28
28
  "@exodus/bitcoin-lib": "^2.4.3",
29
29
  "@exodus/bitcoinjs": "^1.1.0",
@@ -34,7 +34,7 @@
34
34
  "@exodus/simple-retry": "^0.0.6",
35
35
  "bech32": "^1.1.3",
36
36
  "bip32-path": "^0.4.2",
37
- "bs58check": "^2.1.2",
37
+ "bs58check": "^3.0.1",
38
38
  "delay": "^4.0.1",
39
39
  "lodash": "^4.17.21",
40
40
  "minimalistic-assert": "^1.0.1",
@@ -56,5 +56,5 @@
56
56
  "type": "git",
57
57
  "url": "git+https://github.com/ExodusMovement/assets.git"
58
58
  },
59
- "gitHead": "f1b6ccb198231db1fb6c9c51a0173967ec4efd1f"
59
+ "gitHead": "b680523e8d9e053a441e4ce863c83ada389b7a73"
60
60
  }
package/src/index.js CHANGED
@@ -23,7 +23,8 @@ export * from './move-funds.js'
23
23
  export { createEncodeMultisigContract } from './multisig-address.js'
24
24
  export { toAsyncSigner } from './tx-sign/taproot.js'
25
25
  export * from './ordinals-utils.js'
26
- export { signMessage } from './sign-message.js'
26
+ export { signMessage, signMessageWithSigner } from './sign-message.js'
27
+ export { writePsbtBlockHeight, readPsbtBlockHeight } from './psbt-proprietary-types.js'
27
28
 
28
29
  // TODO: remove these, kept for compat
29
30
  export { scriptClassify } from '@exodus/bitcoinjs'
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Set of utilities to add proprietary use types to PSBT
3
+ * see https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#proprietary-use-type
4
+ */
5
+
6
+ import assert from 'minimalistic-assert'
7
+ import varuint from 'varuint-bitcoin'
8
+
9
+ const IDENTIFIER = 'exodus'
10
+ const PROP_TYPE_MARKER = 0xfc
11
+
12
+ export const SubType = Object.freeze({
13
+ BlockHeight: 0x01,
14
+ })
15
+
16
+ const buildPropKey = (subType) => {
17
+ const idBuf = Buffer.from(IDENTIFIER, 'utf8')
18
+ const parts = [
19
+ Buffer.from([PROP_TYPE_MARKER]),
20
+ varuint.encode(idBuf.length),
21
+ idBuf,
22
+ varuint.encode(subType),
23
+ ]
24
+ return Buffer.concat(parts)
25
+ }
26
+
27
+ const u32LE = (n) => {
28
+ const b = Buffer.allocUnsafe(4)
29
+ b.writeUInt32LE(n >>> 0, 0)
30
+ return b
31
+ }
32
+
33
+ const findProprietaryVal = (unknownArr, subType) => {
34
+ if (!unknownArr) return
35
+
36
+ const prefix = buildPropKey(subType)
37
+
38
+ return unknownArr.find((kv) => kv.key.equals(prefix))
39
+ }
40
+
41
+ export const writePsbtBlockHeight = (psbt, height) => {
42
+ assert(
43
+ Number.isInteger(height) && height >= 0 && height <= 4_294_967_295,
44
+ 'blockHeight must be a positive number between 0 and 4294967295'
45
+ )
46
+
47
+ psbt.addUnknownKeyValToGlobal({ key: buildPropKey(SubType.BlockHeight), value: u32LE(height) })
48
+ }
49
+
50
+ export const readPsbtBlockHeight = (psbt) => {
51
+ const kv = findProprietaryVal(psbt.data.globalMap.unknownKeyVals, SubType.BlockHeight)
52
+ return kv ? kv.value.readUInt32LE(0) : undefined
53
+ }
@@ -9,3 +9,13 @@ export const signMessage = async ({ privateKey, message: _message }) => {
9
9
 
10
10
  return Buffer.isBuffer(signedMessage) ? signedMessage : Buffer.from(signedMessage, 'base64')
11
11
  }
12
+
13
+ export const signMessageWithSigner = async ({ signer, message: { bip322Message, ...rest } }) => {
14
+ assert(bip322Message, `expected bip322Message`)
15
+ assert(Object.keys(rest).length === 0, `unexpected message properties`)
16
+
17
+ const { address, message } = bip322Message
18
+ const signedMessage = await Signer.signAsync(signer, address, message)
19
+
20
+ return Buffer.isBuffer(signedMessage) ? signedMessage : Buffer.from(signedMessage, 'base64')
21
+ }
@@ -39,7 +39,18 @@ export const createGetKeyWithMetadata = ({
39
39
  function getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address) {
40
40
  const privateWif = getOwnProperty(privateKeysAddressMap, address, 'string')
41
41
  assert(privateWif, `there is no private key for address ${address}`)
42
- const { privateKey, compressed } = ECPair.fromWIF(privateWif, networkInfo)
42
+
43
+ // ECPair.fromWIF() rejects network objects whose pubKeyHash/scriptHash are wider than one byte (UInt8).
44
+ // so we skip the network argument in that case and let the library infer it from the WIF prefix.
45
+ const useNet =
46
+ networkInfo && networkInfo.pubKeyHash <= 0xff && networkInfo.scriptHash <= 0xff
47
+ ? networkInfo
48
+ : undefined
49
+
50
+ const { privateKey, compressed } = useNet
51
+ ? ECPair.fromWIF(privateWif, useNet)
52
+ : ECPair.fromWIF(privateWif)
53
+
43
54
  const publicKey = privateKeyToPublicKey({ privateKey, compressed, format: 'buffer' })
44
55
  return { privateKey, publicKey, purpose }
45
56
  }
@@ -1,6 +1,8 @@
1
1
  import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/bitcoinjs'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
+ import { writePsbtBlockHeight } from '../psbt-proprietary-types.js'
5
+
4
6
  const _MAXIMUM_FEE_RATES = {
5
7
  qtumignition: 25_000,
6
8
  ravencoin: 1_000_000,
@@ -40,6 +42,7 @@ export function createPrepareForSigning({
40
42
 
41
43
  // Create PSBT based on internal Exodus data structure
42
44
  const psbt = createPsbtFromTxData({
45
+ assetName,
43
46
  ...unsignedTx.txData,
44
47
  ...unsignedTx.txMeta,
45
48
  resolvePurpose,
@@ -68,6 +71,7 @@ function createPsbtFromTxData({
68
71
  assetName,
69
72
  Psbt,
70
73
  Transaction,
74
+ blockHeight,
71
75
  }) {
72
76
  // use harcoded max fee rates for specific assets
73
77
  // if undefined, will be set to default value by PSBT (2500)
@@ -75,6 +79,11 @@ function createPsbtFromTxData({
75
79
 
76
80
  const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
77
81
 
82
+ // If present, add blockHeight as a proprietary field
83
+ if (blockHeight) {
84
+ writePsbtBlockHeight(psbt, blockHeight)
85
+ }
86
+
78
87
  // Fill tx
79
88
  for (const { txId, vout, address, value, script, sequence, tapLeafScript } of inputs) {
80
89
  // TODO: don't use the purpose as intermediate variable
@@ -83,9 +92,15 @@ function createPsbtFromTxData({
83
92
 
84
93
  const isSegwitAddress = purpose === 84
85
94
  const isTaprootAddress = purpose === 86
95
+ const isZcashAsset = assetName === 'zcash'
86
96
 
87
97
  const txIn = { hash: txId, index: vout, sequence }
88
98
 
99
+ if (isZcashAsset) {
100
+ txIn.script = script
101
+ txIn.value = value
102
+ }
103
+
89
104
  if (isTaprootAddress && tapLeafScript) {
90
105
  txIn.tapLeafScript = tapLeafScript
91
106
  }
@@ -97,12 +112,9 @@ function createPsbtFromTxData({
97
112
 
98
113
  const rawTx = (rawTxs || []).find((t) => t.txId === txId)
99
114
 
100
- // Non-taproot outputs require the full transaction
101
- assert(
102
- isTaprootAddress || !!rawTx?.rawData,
103
- `Non-taproot outputs require the full previous transaction.`
104
- )
105
- if (!isTaprootAddress) {
115
+ if (!isTaprootAddress && !isZcashAsset) {
116
+ assert(!!rawTx?.rawData, `Non-taproot outputs require the full previous transaction.`)
117
+
106
118
  const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
107
119
  if (canParseTx(Transaction, rawTxBuffer)) {
108
120
  txIn.nonWitnessUtxo = rawTxBuffer
@@ -47,6 +47,19 @@ export function toAsyncSigner({ privateKey, publicKey, isTaprootKeySpend }) {
47
47
  extraEntropy: defaultEntropy.getSchnorrEntropy(), // mockable with jest.spyOn
48
48
  format: 'buffer',
49
49
  }),
50
+ signEncoded: async ({ data, enc = 'sig' }) => {
51
+ assert(
52
+ ['der', 'sig'].includes(enc),
53
+ 'signBuffer: invalid encoding type. Expected "der" or "sig".'
54
+ )
55
+ return secp256k1.ecdsaSignHash({
56
+ hash: data,
57
+ der: enc === 'der',
58
+ privateKey,
59
+ extraEntropy: null,
60
+ format: 'buffer',
61
+ })
62
+ },
50
63
  publicKey,
51
64
  privateKey,
52
65
  }
@@ -79,6 +92,9 @@ export async function toAsyncBufferSigner({ signer, keyId, isTaprootKeySpend })
79
92
  extraEntropy: defaultEntropy.getSchnorrEntropy(),
80
93
  })
81
94
  },
95
+ signEncoded: async ({ data, enc = 'sig' }) => {
96
+ return signer.sign({ data, keyId, enc, signatureType: 'ecdsa' })
97
+ },
82
98
  publicKey,
83
99
  }
84
100
  }