@exodus/bitcoin-api 2.5.0-alpha.0 → 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.5.0-alpha.0",
3
+ "version": "2.5.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -39,7 +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
  },
44
- "gitHead": "04641210f5ac2a4b490cf348b2917b28cf463ba2"
46
+ "gitHead": "d7b87308c1645a96ba4451f0b40b08ae3c1c29e5"
45
47
  }
@@ -1,5 +1,5 @@
1
1
  import secp256k1 from '@exodus/secp256k1'
2
- import { toXOnly } from '../../tx-sign/taproot'
2
+ import { toXOnly } from '../ecc-utils'
3
3
 
4
4
  /**
5
5
  * Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
@@ -0,0 +1,3 @@
1
+ export const toXOnly = (publicKey) => {
2
+ return publicKey.slice(1, 33)
3
+ }
@@ -1,5 +1,4 @@
1
1
  import { payments } from 'bitcoinjs-lib'
2
- import assert from 'minimalistic-assert'
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
@@ -8,14 +8,12 @@ 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) => {
@@ -6,7 +6,8 @@ import { identity, pickBy } from 'lodash'
6
6
  import * as defaultBitcoinjsLib from 'bitcoinjs-lib'
7
7
  import secp256k1 from 'secp256k1'
8
8
  import { hash160 } from './hash-utils'
9
- import { toXOnly } from './tx-sign/taproot'
9
+ import { toXOnly } from './bitcoinjs-lib/ecc-utils'
10
+ import { eccFactory } from './bitcoinjs-lib/ecc'
10
11
 
11
12
  export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
12
13
  const payload = Buffer.concat([Buffer.from([p2pkh]), hash160(publicKey)])
@@ -16,14 +17,13 @@ export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
16
17
  export const createBtcLikeKeys = ({
17
18
  coinInfo,
18
19
  versions,
19
- ecc,
20
20
  useBip86 = false,
21
21
  bitcoinjsLib = defaultBitcoinjsLib,
22
22
  extraFunctions = {},
23
23
  }) => {
24
24
  assert(coinInfo, 'coinInfo is required')
25
25
  assert(versions, 'versions is required')
26
- assert(ecc, 'ecc is required')
26
+ const ecc = eccFactory()
27
27
  const {
28
28
  encodePrivate: encodePrivateCustom,
29
29
  encodePublic: encodePublicCustom,
@@ -91,10 +91,7 @@ export const createBtcLikeKeys = ({
91
91
  (useBip86
92
92
  ? (publicKey: Buffer): string => {
93
93
  const network = coinInfo.toBitcoinJS()
94
- return bitcoinjsLib.payments.p2tr(
95
- { internalPubkey: toXOnly(publicKey), network },
96
- { eccLib: ecc }
97
- ).address
94
+ return bitcoinjsLib.payments.p2tr({ internalPubkey: toXOnly(publicKey), network }).address
98
95
  }
99
96
  : undefined)
100
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'
@@ -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) {
@@ -6,7 +6,9 @@ import { getOwnProperty } from '@exodus/basic-utils'
6
6
 
7
7
  import secp256k1 from 'secp256k1'
8
8
 
9
- import { toAsyncSigner, toXOnly, tweakSigner } from './taproot'
9
+ import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
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)
@@ -78,44 +123,27 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
78
123
  return { key, publicKey, purpose }
79
124
  })
80
125
 
81
- // Fill tx
82
- for (const { txId, vout, address, value, script, sequence } of inputs) {
83
- const { purpose, publicKey } = getKeyAndPurpose(address)
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
84
134
 
85
- const isSegwitAddress = purpose === 84
86
- const isTaprootAddress = purpose === 86
87
- const txIn = { hash: txId, index: vout, sequence }
88
- if (isSegwitAddress || isTaprootAddress) {
89
- // witness outputs only require the value and the script, not the full transaction
90
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
91
- if (isTaprootAddress) {
92
- txIn.tapInternalKey = toXOnly(publicKey)
93
- }
94
- } else {
95
- const rawTx = (rawTxs || []).find((t) => t.txId === txId)
96
- // non-witness outptus require the full transaction
97
- assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
98
- const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
99
- if (canParseTx(rawTxBuffer)) {
100
- txIn.nonWitnessUtxo = rawTxBuffer
101
- } else {
102
- // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
103
- console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
104
- psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
105
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
106
- }
107
- }
108
- psbt.addInput(txIn)
109
- }
135
+ if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
110
136
 
111
- for (const [address, amount] of outputs) {
112
- psbt.addOutput({ value: amount, address })
113
- }
137
+ const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
114
138
 
115
139
  // The Taproot SIGHASH flag includes all previous outputs,
116
140
  // so signing is only done AFTER all inputs have been updated
117
- for (let index = 0; index < inputs.length; index++) {
118
- const { address } = inputs[index]
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
119
147
  const { key, purpose, publicKey } = getKeyAndPurpose(address)
120
148
 
121
149
  if (purpose === 49) {
@@ -130,19 +158,19 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
130
158
  })
131
159
  }
132
160
 
133
- if (ecc.signSchnorrAsync) {
134
- // desktop / BE / mobile with bip-schnorr signing
135
- const isTaprootAddress = purpose === 86
136
- const signingKey = isTaprootAddress
137
- ? tweakSigner({ signer: key, ECPair, ecc, network })
138
- : key
139
- await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey, ecc }))
140
- } else {
141
- // mobile signing
142
- psbt.signInput(index, key)
143
- }
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)
144
165
  }
145
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
+ }
146
174
  // Serialize tx
147
175
  psbt.finalizeAllInputs()
148
176
  const tx = psbt.extractTransaction()
@@ -1,11 +1,14 @@
1
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!')
@@ -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)
@@ -48,7 +50,3 @@ export function toAsyncSigner({ keyPair, ecc }) {
48
50
  }
49
51
  return keyPair
50
52
  }
51
-
52
- export const toXOnly = (publicKey) => {
53
- return publicKey.slice(1, 33)
54
- }