@exodus/bitcoin-api 2.4.1 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -22,13 +22,13 @@
22
22
  "@exodus/basic-utils": "^2.0.1",
23
23
  "@exodus/bip-schnorr": "0.6.6-fork-1",
24
24
  "@exodus/bip44-constants": "^195.0.0",
25
- "@exodus/bitcoinjs-lib": "6.0.2-beta.5",
26
25
  "@exodus/models": "^8.10.4",
27
26
  "@exodus/secp256k1": "4.0.2-exodus.0",
28
27
  "@exodus/simple-retry": "0.0.6",
29
28
  "@exodus/timer": "^1.0.0",
30
29
  "@noble/secp256k1": "~1.5.3",
31
30
  "bech32": "^1.1.3",
31
+ "bitcoinjs-lib": "^6.1.5",
32
32
  "coininfo": "5.1.0",
33
33
  "delay": "4.0.1",
34
34
  "ecpair": "2.0.1",
@@ -39,6 +39,9 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@exodus/bitcoin-meta": "^1.0.1",
42
+ "@scure/base": "^1.1.3",
43
+ "@scure/btc-signer": "^1.1.0",
42
44
  "jest-when": "^3.5.1"
43
- }
45
+ },
46
+ "gitHead": "d7b87308c1645a96ba4451f0b40b08ae3c1c29e5"
44
47
  }
@@ -1,4 +1,5 @@
1
1
  import secp256k1 from '@exodus/secp256k1'
2
+ import { toXOnly } from '../ecc-utils'
2
3
 
3
4
  /**
4
5
  * Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
@@ -24,7 +25,7 @@ export const common = {
24
25
  const t = secp256k1.publicKeyTweakAdd(toPubKey(p), tweak)
25
26
  return {
26
27
  parity: t[0] === 0x02 ? 0 : 1,
27
- xOnlyPubkey: t.slice(1, 33),
28
+ xOnlyPubkey: toXOnly(t),
28
29
  }
29
30
  } catch (err) {
30
31
  return null
@@ -1,4 +1,4 @@
1
- import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
1
+ import { TinySecp256k1Interface, initEccLib } from 'bitcoinjs-lib'
2
2
  import { Point, schnorr, sign } from '@noble/secp256k1'
3
3
  import { common, toPubKey } from './common'
4
4
 
@@ -48,3 +48,5 @@ export const desktopEcc: TinySecp256k1Interface = {
48
48
  pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
49
49
  Point.fromHex(p).toRawBytes(compressed),
50
50
  }
51
+
52
+ initEccLib(desktopEcc)
@@ -1,4 +1,4 @@
1
- import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
1
+ import { TinySecp256k1Interface, initEccLib } from 'bitcoinjs-lib'
2
2
  import secp256k1 from '@exodus/secp256k1'
3
3
  // TODO: temp import until '@noble/secp256k1' can be used
4
4
  import { isPoint } from 'tiny-secp256k1'
@@ -56,3 +56,5 @@ export const mobileEcc: TinySecp256k1Interface = {
56
56
  pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
57
57
  secp256k1.publicKeyConvert(p, compressed),
58
58
  }
59
+
60
+ initEccLib(mobileEcc)
@@ -0,0 +1,3 @@
1
+ export const toXOnly = (publicKey) => {
2
+ return publicKey.slice(1, 33)
3
+ }
@@ -1,5 +1,4 @@
1
- import { payments } from '@exodus/bitcoinjs-lib'
2
- import assert from 'minimalistic-assert'
1
+ import { payments } from 'bitcoinjs-lib'
3
2
 
4
3
  function isPaymentFactory(payment: any): (script: Buffer, eccLib?: any) => boolean {
5
4
  return (script: Buffer, eccLib?: any): boolean => {
@@ -31,10 +30,9 @@ const types = {
31
30
  NONSTANDARD: 'nonstandard',
32
31
  }
33
32
 
34
- const outputFactory = ({ ecc }) => (script: Buffer) => {
35
- assert(ecc, 'ecc is required')
33
+ const outputFactory = () => (script: Buffer) => {
36
34
  if (isP2WPKH(script)) return types.P2WPKH
37
- if (isP2TR(script, ecc)) return types.P2TR
35
+ if (isP2TR(script)) return types.P2TR
38
36
  if (isP2PKH(script)) return types.P2PKH
39
37
  if (isP2MS(script)) return types.P2MS
40
38
  if (isP2PK(script)) return types.P2PK
@@ -2,20 +2,18 @@ import bs58check from 'bs58check'
2
2
  import * as bech32 from 'bech32'
3
3
  import assert from 'minimalistic-assert'
4
4
  import { identity, pickBy } from 'lodash'
5
- import * as bitcoinjsOriginal from '@exodus/bitcoinjs-lib'
5
+ import * as bitcoinjsOriginal from 'bitcoinjs-lib'
6
6
 
7
7
  export const createBtcLikeAddress = ({
8
8
  versions,
9
9
  coinInfo,
10
10
  bitcoinjsLib: bitcoinjsLibFork,
11
- ecc,
12
11
  useBip86 = false,
13
12
  validateFunctions = {},
14
13
  extraFunctions = {},
15
14
  }) => {
16
15
  assert(versions, 'versions is required')
17
16
  assert(coinInfo, 'coinInfo is required')
18
- assert(ecc, 'ecc is required')
19
17
 
20
18
  const bs58validateFactory = (version) =>
21
19
  version === undefined
@@ -55,7 +53,7 @@ export const createBtcLikeAddress = ({
55
53
  ((addr) => {
56
54
  try {
57
55
  const network = coinInfo.toBitcoinJS()
58
- bitcoinjsLibFork.payments.p2tr({ address: addr, network }, { eccLib: ecc })
56
+ bitcoinjsLibFork.payments.p2tr({ address: addr, network })
59
57
  return true
60
58
  } catch (e) {
61
59
  return false
@@ -91,7 +89,7 @@ export const createBtcLikeAddress = ({
91
89
 
92
90
  const toScriptPubKey = (string) => {
93
91
  const network = coinInfo.toBitcoinJS()
94
- return bitcoinjsOriginal.address.toOutputScript(string, network, ecc)
92
+ return bitcoinjsOriginal.address.toOutputScript(string, network)
95
93
  }
96
94
 
97
95
  const fromScriptPubKey = (scriptPubKey) => {
@@ -3,9 +3,11 @@ import wif from 'wif'
3
3
  import * as bech32 from 'bech32'
4
4
  import assert from 'minimalistic-assert'
5
5
  import { identity, pickBy } from 'lodash'
6
- import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
6
+ import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
7
7
  import secp256k1 from 'secp256k1'
8
8
  import { hash160 } from './hash-utils'
9
+ import { toXOnly } from './bitcoinjs-lib/ecc-utils'
10
+ import { eccFactory } from './bitcoinjs-lib/ecc'
9
11
 
10
12
  export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
11
13
  const payload = Buffer.concat([Buffer.from([p2pkh]), hash160(publicKey)])
@@ -15,14 +17,13 @@ export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
15
17
  export const createBtcLikeKeys = ({
16
18
  coinInfo,
17
19
  versions,
18
- ecc,
19
20
  useBip86 = false,
20
21
  bitcoinjsLib = defaultBitcoinjsLib,
21
22
  extraFunctions = {},
22
23
  }) => {
23
24
  assert(coinInfo, 'coinInfo is required')
24
25
  assert(versions, 'versions is required')
25
- assert(ecc, 'ecc is required')
26
+ const ecc = eccFactory()
26
27
  const {
27
28
  encodePrivate: encodePrivateCustom,
28
29
  encodePublic: encodePublicCustom,
@@ -90,10 +91,7 @@ export const createBtcLikeKeys = ({
90
91
  (useBip86
91
92
  ? (publicKey: Buffer): string => {
92
93
  const network = coinInfo.toBitcoinJS()
93
- return bitcoinjsLib.payments.p2tr(
94
- { internalPubkey: publicKey.slice(1, 33), network },
95
- { eccLib: ecc }
96
- ).address
94
+ return bitcoinjsLib.payments.p2tr({ internalPubkey: toXOnly(publicKey), network }).address
97
95
  }
98
96
  : undefined)
99
97
 
@@ -53,12 +53,11 @@ const scriptPubKeyLengths = {
53
53
  // 10 = version: 4, locktime: 4, inputs and outputs count: 1
54
54
  // 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
55
55
  // 34 = value: 8, count: 1, scriptPubKey: 25 (P2PKH) and 23 (P2SH)
56
- export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => {
57
- assert(ecc, 'ecc is required')
56
+ export const getSizeFactory = ({ defaultOutputType, addressApi }) => {
58
57
  assert(defaultOutputType, 'defaultOutputType is required')
59
58
  assert(addressApi, 'addressApi is required')
60
59
 
61
- const scriptClassifier = scriptClassifierFactory({ ecc, addressApi })
60
+ const scriptClassifier = scriptClassifierFactory({ addressApi })
62
61
 
63
62
  return (
64
63
  asset: Object,
@@ -166,8 +165,8 @@ export const getSizeFactory = ({ ecc, defaultOutputType, addressApi }) => {
166
165
  }
167
166
  }
168
167
 
169
- const getFeeEstimatorFactory = ({ ecc, defaultOutputType, addressApi }) => {
170
- const getSize = getSizeFactory({ ecc, defaultOutputType, addressApi })
168
+ const getFeeEstimatorFactory = ({ defaultOutputType, addressApi }) => {
169
+ const getSize = getSizeFactory({ defaultOutputType, addressApi })
171
170
  return createDefaultFeeEstimator(getSize)
172
171
  }
173
172
  export default getFeeEstimatorFactory
@@ -16,11 +16,10 @@ const hashStringIfTooBig = (str) =>
16
16
  .slice(0, maxSize)
17
17
  : str
18
18
 
19
- export const scriptClassifierFactory = ({ addressApi, ecc }) => {
20
- assert(ecc, 'ecc is required')
19
+ export const scriptClassifierFactory = ({ addressApi }) => {
21
20
  assert(addressApi, 'addressApi is required')
22
21
 
23
- const classifyOutput = scriptClassify.outputFactory({ ecc })
22
+ const classifyOutput = scriptClassify.outputFactory()
24
23
 
25
24
  const classifyScriptHex = memoizeLruCache(
26
25
  ({ assetName, script }) => {
@@ -42,6 +41,7 @@ export const scriptClassifierFactory = ({ addressApi, ecc }) => {
42
41
  else if (addressApi.isP2TR && addressApi.isP2TR(address)) return P2TR
43
42
  else if (addressApi.isP2WSH && addressApi.isP2WSH(address)) return P2WSH
44
43
  return classifyScriptHex({
44
+ assetName,
45
45
  classifyOutput,
46
46
  script: addressApi.toScriptPubKey(address).toString('hex'),
47
47
  })
package/src/index.js CHANGED
@@ -16,3 +16,5 @@ export * from './unconfirmed-ancestor-data'
16
16
  export * from './parse-unsigned-tx'
17
17
  export * from './insight-api-client/util'
18
18
  export * from './move-funds'
19
+ export { toAsyncSigner } from './tx-sign/taproot'
20
+ export { toXOnly } from './bitcoinjs-lib/ecc-utils'
@@ -2,7 +2,7 @@ import assert from 'minimalistic-assert'
2
2
  import BIPPath from 'bip32-path'
3
3
  import BN from 'bn.js'
4
4
  import lodash from 'lodash'
5
- import { Transaction as BitcoinTransactionClass } from '@exodus/bitcoinjs-lib'
5
+ import { Transaction as BitcoinTransactionClass } from 'bitcoinjs-lib'
6
6
 
7
7
  export const parseUnsignedTxFactory = ({ Transaction = BitcoinTransactionClass } = {}) => async ({
8
8
  asset,
@@ -16,7 +16,7 @@ import {
16
16
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
17
17
  import { getOrdinalsUtxos, getUsableUtxos, getUtxos } from '../utxos-utils'
18
18
 
19
- import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
19
+ import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
20
20
 
21
21
  const ASSETS_SUPPORTED_BIP_174 = [
22
22
  'bitcoin',
@@ -305,6 +305,8 @@ export const createAndBroadcastTXFactory = ({
305
305
  } else {
306
306
  outputs = []
307
307
  }
308
+
309
+ let sendOutput
308
310
  if (address) {
309
311
  if (transferOrdinalsUtxos) {
310
312
  outputs.push(
@@ -313,7 +315,8 @@ export const createAndBroadcastTXFactory = ({
313
315
  .map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
314
316
  )
315
317
  } else {
316
- outputs.push(createOutput(assetName, address, sendAmount))
318
+ sendOutput = createOutput(assetName, address, sendAmount)
319
+ outputs.push(sendOutput)
317
320
  }
318
321
  }
319
322
 
@@ -408,17 +411,23 @@ export const createAndBroadcastTXFactory = ({
408
411
  }
409
412
  }
410
413
 
411
- let changeUtxoIndex = -1
412
- if (changeOutput) {
413
- for (let i = 0; i < outputs.length; i++) {
414
- const [address, amount] = outputs[i]
415
- if (changeOutput[0] === address && changeOutput[1] === amount) {
416
- changeUtxoIndex = i
417
- break
414
+ function findUtxoIndex(output) {
415
+ let utxoIndex = -1
416
+ if (output) {
417
+ for (let i = 0; i < outputs.length; i++) {
418
+ const [address, amount] = outputs[i]
419
+ if (output[0] === address && output[1] === amount) {
420
+ utxoIndex = i
421
+ break
422
+ }
418
423
  }
419
424
  }
425
+ return utxoIndex
420
426
  }
421
427
 
428
+ const changeUtxoIndex = findUtxoIndex(changeOutput)
429
+ const sendUtxoIndex = findUtxoIndex(sendOutput)
430
+
422
431
  const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
423
432
 
424
433
  let remainingUtxos = usableUtxos.difference(selectedUtxos)
@@ -518,7 +527,12 @@ export const createAndBroadcastTXFactory = ({
518
527
  })
519
528
  }
520
529
 
521
- return { txId, replacedTxId: replaceTx?.txId }
530
+ return {
531
+ txId,
532
+ sendUtxoIndex,
533
+ sendAmount: sendAmount.toBaseNumber(),
534
+ replacedTxId: replaceTx?.txId,
535
+ }
522
536
  }
523
537
 
524
538
  export function createInputs(assetName, ...rest) {
@@ -1,12 +1,14 @@
1
1
  import assert from 'minimalistic-assert'
2
2
  import lodash from 'lodash'
3
3
  import ECPairFactory from 'ecpair'
4
- import { payments, Psbt, Transaction } from '@exodus/bitcoinjs-lib'
4
+ import { payments, Psbt, Transaction } from 'bitcoinjs-lib'
5
5
  import { getOwnProperty } from '@exodus/basic-utils'
6
6
 
7
7
  import secp256k1 from 'secp256k1'
8
8
 
9
+ import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
9
10
  import { toAsyncSigner, tweakSigner } from './taproot'
11
+ import { eccFactory } from '../bitcoinjs-lib/ecc'
10
12
 
11
13
  let ECPair
12
14
 
@@ -34,28 +36,71 @@ export const serializeTx = ({ tx }) => {
34
36
  }
35
37
  }
36
38
 
37
- export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network, ecc }) => {
39
+ // Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
40
+ function createPSBT({ inputs, outputs, rawTxs, networkInfo, getKeyAndPurpose, assetName }) {
41
+ // use harcoded max fee rates for specific assets
42
+ // if undefined, will be set to default value by PSBT (2500)
43
+ const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
44
+
45
+ const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
46
+
47
+ // Fill tx
48
+ for (const { txId, vout, address, value, script, sequence } of inputs) {
49
+ const { purpose, publicKey } = getKeyAndPurpose(address)
50
+
51
+ const isSegwitAddress = purpose === 84
52
+ const isTaprootAddress = purpose === 86
53
+ const txIn = { hash: txId, index: vout, sequence }
54
+ if (isSegwitAddress || isTaprootAddress) {
55
+ // witness outputs only require the value and the script, not the full transaction
56
+ txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
57
+ if (isTaprootAddress) {
58
+ txIn.tapInternalKey = toXOnly(publicKey)
59
+ }
60
+ } else {
61
+ const rawTx = (rawTxs || []).find((t) => t.txId === txId)
62
+ // non-witness outptus require the full transaction
63
+ assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
64
+ const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
65
+ if (canParseTx(rawTxBuffer)) {
66
+ txIn.nonWitnessUtxo = rawTxBuffer
67
+ } else {
68
+ // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
69
+ console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
70
+ psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
71
+ txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
72
+ }
73
+ }
74
+ psbt.addInput(txIn)
75
+ }
76
+
77
+ for (const [address, amount] of outputs) {
78
+ psbt.addOutput({ value: amount, address })
79
+ }
80
+
81
+ return psbt
82
+ }
83
+
84
+ // Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
85
+ function createPSBTFromBuffer({ psbtBuffer, ecc }) {
86
+ const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
87
+
88
+ return psbt
89
+ }
90
+
91
+ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network }) => {
38
92
  assert(assetName, 'assetName is required')
39
93
  assert(resolvePurpose, 'resolvePurpose is required')
40
94
  assert(keys, 'keys is required')
41
95
  assert(coinInfo, 'coinInfo is required')
42
- assert(ecc, 'ecc is required')
96
+
43
97
  return async ({ unsignedTx, hdkeys, privateKeysAddressMap }): Object => {
44
98
  assert(unsignedTx, 'unsignedTx is required')
45
99
  assert(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
46
- const { addressPathsMap, rawTxs } = unsignedTx.txMeta
47
- const { inputs, outputs } = unsignedTx.txData
100
+ const { addressPathsMap } = unsignedTx.txMeta
48
101
  const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
49
102
 
50
- // use harcoded max fee rates for specific assets
51
- // if undefined, will be set to default value by PSBT (2500)
52
- const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
53
-
54
- const psbt = new Psbt({ maximumFeeRate, eccLib: ecc, network: networkInfo })
55
-
56
- if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
57
-
58
- ECPair = ECPair || ECPairFactory(ecc)
103
+ ECPair = ECPair || ECPairFactory(eccFactory())
59
104
 
60
105
  const getKeyAndPurpose = lodash.memoize((address) => {
61
106
  const purpose = resolvePurpose(address)
@@ -63,7 +108,8 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
63
108
  const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
64
109
  assert(privateKey, `there is no private key for address ${address}`)
65
110
  const key = ECPair.fromWIF(privateKey, networkInfo)
66
- return { key, purpose }
111
+ const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
112
+ return { key, purpose, publicKey }
67
113
  }
68
114
  const path = addressPathsMap[address]
69
115
  assert(hdkeys, 'hdkeys must be provided')
@@ -72,51 +118,39 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
72
118
  assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
73
119
  const derivedhdkey = hdkey.derive(path)
74
120
  const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
75
- return { key: ECPair.fromWIF(privateEncoded, networkInfo), purpose }
121
+ const key = ECPair.fromWIF(privateEncoded, networkInfo)
122
+ const publicKey = derivedhdkey.publicKey
123
+ return { key, publicKey, purpose }
76
124
  })
77
125
 
78
- // Fill tx
79
- for (const { txId, vout, address, value, script, sequence } of inputs) {
80
- const { purpose } = getKeyAndPurpose(address)
81
- const isSegwitAddress = purpose === 84
82
- const isTaprootAddress = purpose === 86
83
- const txIn = { hash: txId, index: vout, sequence }
84
- if (isSegwitAddress || isTaprootAddress) {
85
- // witness outputs only require the value and the script, not the full transaction
86
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
87
- } else {
88
- const rawTx = (rawTxs || []).find((t) => t.txId === txId)
89
- // non-witness outptus require the full transaction
90
- assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
91
- const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
92
- if (canParseTx(rawTxBuffer)) {
93
- txIn.nonWitnessUtxo = rawTxBuffer
94
- } else {
95
- // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
96
- console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
97
- psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
98
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
99
- }
100
- }
101
- psbt.addInput(txIn)
102
- }
126
+ const isPsbtBufferPassed =
127
+ unsignedTx.txData.psbtBuffer &&
128
+ unsignedTx.txMeta.addressPathsMap &&
129
+ unsignedTx.txMeta.inputsToSign
130
+ const psbt = isPsbtBufferPassed
131
+ ? createPSBTFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
132
+ : createPSBT({ ...unsignedTx.txData, ...unsignedTx.txMeta, getKeyAndPurpose, networkInfo })
133
+ const { inputs } = unsignedTx.txData
103
134
 
104
- for (const [address, amount] of outputs) {
105
- psbt.addOutput({ value: amount, address })
106
- }
135
+ if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
136
+
137
+ const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
107
138
 
108
139
  // The Taproot SIGHASH flag includes all previous outputs,
109
140
  // so signing is only done AFTER all inputs have been updated
110
- for (let index = 0; index < inputs.length; index++) {
111
- const { address } = inputs[index]
112
- const { key, purpose } = getKeyAndPurpose(address)
141
+ for (let index = 0; index < psbt.inputCount; index++) {
142
+ const inputInfo = inputsToSign[index]
143
+ // dApps request to sign only specific transaction inputs.
144
+ if (!inputInfo) continue
145
+ const { address, sigHash } = inputInfo
146
+ const sigHashTypes = sigHash ? [sigHash] : undefined
147
+ const { key, purpose, publicKey } = getKeyAndPurpose(address)
113
148
 
114
149
  if (purpose === 49) {
115
150
  // If spending from a P2SH address, we assume the address is P2SH wrapping
116
151
  // P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
117
152
  // signing a P2SH input if we are importing a private key
118
153
  // BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
119
- const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
120
154
  const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
121
155
  const p2sh = payments.p2sh({ redeem: p2wpkh })
122
156
  psbt.updateInput(index, {
@@ -124,19 +158,19 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
124
158
  })
125
159
  }
126
160
 
127
- if (ecc.signSchnorrAsync) {
128
- // desktop / BE / mobile with bip-schnorr signing
129
- const isTaprootAddress = purpose === 86
130
- const signingKey = isTaprootAddress
131
- ? tweakSigner({ signer: key, ECPair, ecc, network })
132
- : key
133
- await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey, ecc }))
134
- } else {
135
- // mobile signing
136
- psbt.signInput(index, key)
137
- }
161
+ // desktop / BE / mobile with bip-schnorr signing
162
+ const isTaprootAddress = purpose === 86
163
+ const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
164
+ await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), sigHashTypes)
138
165
  }
139
166
 
167
+ // If a dapp authored the TX, it expects a serialized PSBT response.
168
+ // Note: we wouldn't be able to finalise inputs in some cases that's why we serialize before finalizing inputs.
169
+ if (isPsbtBufferPassed) {
170
+ const rawPSBT = psbt.toBuffer()
171
+
172
+ return { plainTx: { rawPSBT } }
173
+ }
140
174
  // Serialize tx
141
175
  psbt.finalizeAllInputs()
142
176
  const tx = psbt.extractTransaction()
@@ -1,11 +1,14 @@
1
- import { crypto } from '@exodus/bitcoinjs-lib'
1
+ import { crypto } from 'bitcoinjs-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
  import { getSchnorrEntropy } from './default-entropy'
4
+ import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
5
+ import { eccFactory } from '../bitcoinjs-lib/ecc'
4
6
 
5
- export function tweakSigner({ signer, ECPair, ecc, tweakHash, network }) {
7
+ const ecc = eccFactory()
8
+
9
+ export function tweakSigner({ signer, ECPair, tweakHash, network }) {
6
10
  assert(signer, 'signer is required')
7
11
  assert(ECPair, 'ECPair is required')
8
- assert(ecc, 'ecc is required')
9
12
  let privateKey: Uint8Array | undefined = signer.privateKey
10
13
  if (!privateKey) {
11
14
  throw new Error('Private key is required for tweaking signer!')
@@ -16,7 +19,7 @@ export function tweakSigner({ signer, ECPair, ecc, tweakHash, network }) {
16
19
 
17
20
  const tweakedPrivateKey = ecc.privateAdd(
18
21
  privateKey,
19
- tapTweakHash(signer.publicKey.slice(1, 33), tweakHash)
22
+ tapTweakHash(toXOnly(signer.publicKey), tweakHash)
20
23
  )
21
24
  if (!tweakedPrivateKey) {
22
25
  throw new Error('Invalid tweaked private key!')
@@ -34,9 +37,8 @@ function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
34
37
  /**
35
38
  * Take a sync signer and make it async.
36
39
  */
37
- export function toAsyncSigner({ keyPair, ecc }) {
40
+ export function toAsyncSigner({ keyPair }) {
38
41
  assert(keyPair, 'keyPair is required')
39
- assert(ecc, 'ecc is required')
40
42
  keyPair.sign = async (h) => {
41
43
  const sig = await ecc.signAsync(h, keyPair.privateKey)
42
44
  return Buffer.from(sig)