@exodus/bitcoin-api 2.3.2 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@exodus/asset-lib": "^3.7.2",
22
+ "@exodus/basic-utils": "^2.0.1",
22
23
  "@exodus/bip44-constants": "^195.0.0",
23
24
  "@exodus/bitcoinjs-lib": "6.0.2-beta.5",
24
25
  "@exodus/models": "^8.10.4",
@@ -35,10 +36,11 @@
35
36
  "url-join": "4.0.0"
36
37
  },
37
38
  "devDependencies": {
39
+ "@exodus/asset-lib": "^3.7.1",
38
40
  "@exodus/bcash-meta": "^1.0.0",
39
41
  "@exodus/bip-schnorr": "0.6.6-fork-1",
40
42
  "@exodus/bitcoin-meta": "^1.0.1",
41
43
  "@noble/secp256k1": "~1.5.3"
42
44
  },
43
- "gitHead": "5bc09581d58d29a80871f4fbb189ff2dcf389b96"
45
+ "gitHead": "ae212da85b29d90a3656bcfa034608950f883df1"
44
46
  }
@@ -1,5 +1,5 @@
1
1
  import bs58check from 'bs58check'
2
- import bech32 from 'bech32'
2
+ import * as bech32 from 'bech32'
3
3
  import assert from 'minimalistic-assert'
4
4
  import { identity, pickBy } from 'lodash'
5
5
  import * as bitcoinjsOriginal from '@exodus/bitcoinjs-lib'
@@ -1,6 +1,6 @@
1
1
  import bs58check from 'bs58check'
2
2
  import wif from 'wif'
3
- import bech32 from 'bech32'
3
+ import * as bech32 from 'bech32'
4
4
  import assert from 'minimalistic-assert'
5
5
  import { identity, pickBy } from 'lodash'
6
6
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
package/src/move-funds.js CHANGED
@@ -41,13 +41,20 @@ export const moveFundsFactory = ({
41
41
  assert(address, 'address is required')
42
42
  assert(signTx, 'signTx is required')
43
43
  assert(getAddressesFromPrivateKey, 'getAddressesFromPrivateKey is required')
44
- async function prepareFunds(assetName, input, options = {}) {
45
- const { toAddress, assetClientInterface, MoveFundsError, walletAccount } = options
46
- assert(MoveFundsError, 'MoveFundsError is required') // should we move MoveFundsError to asset libs?
44
+
45
+ async function prepareSendFundsTx({
46
+ assetName,
47
+ walletAccount,
48
+ input,
49
+ toAddress,
50
+ assetClientInterface,
51
+ MoveFundsError,
52
+ }) {
53
+ assert(asset.name === assetName, `expected asset ${asset.name} but got assetName ${assetName}`)
54
+ assert(walletAccount, 'walletAccount is required')
47
55
  assert(toAddress, 'toAddress is required')
48
56
  assert(assetClientInterface, 'assetClientInterface is required')
49
- assert(walletAccount, 'walletAccount is required')
50
- assert(asset.name === assetName, `expected asset ${asset.name} but got assetName ${assetName}`)
57
+ assert(MoveFundsError, 'MoveFundsError is required') // should we move MoveFundsError to asset libs?
51
58
 
52
59
  const formatProps = {
53
60
  asset,
@@ -69,71 +76,78 @@ export const moveFundsFactory = ({
69
76
  multiAddressMode: true,
70
77
  })
71
78
 
72
- let found, address, utxos
73
- for (address of addresses) {
74
- const selfSend = receiveAddresses.some(
75
- (receiveAddress) => String(receiveAddress) === String(address)
76
- )
77
- if (selfSend) {
78
- throw new MoveFundsError('private-key-own-key', formatProps)
79
- }
79
+ const findFromAddress = async () => {
80
+ for (const currentAddress of addresses) {
81
+ const selfSend = receiveAddresses.some(
82
+ (receiveAddress) => String(receiveAddress) === String(currentAddress)
83
+ )
84
+ if (selfSend) {
85
+ throw new MoveFundsError('private-key-own-key', formatProps)
86
+ }
80
87
 
81
- utxos = await getUtxos({ asset, address })
88
+ const utxos = await getUtxos({ asset, address: currentAddress })
82
89
 
83
- if (!utxos.value.isZero) {
84
- found = true
85
- break
90
+ if (!utxos.value.isZero) {
91
+ return { fromAddress: currentAddress, utxos }
92
+ }
86
93
  }
94
+ throw new MoveFundsError('balance-zero', {
95
+ ...formatProps,
96
+ fromAddress: `${addresses.slice(0, -1).join(', ')}, or ${addresses[addresses.length - 1]}`,
97
+ })
87
98
  }
88
- if (!found) {
89
- formatProps.fromAddress = `${addresses.slice(0, -1).join(', ')}, or ${
90
- addresses[addresses.length - 1]
91
- }`
92
- throw new MoveFundsError('balance-zero', formatProps)
93
- }
94
- const fromAddress = address
95
- formatProps.fromAddress = fromAddress
96
99
 
100
+ const { fromAddress, utxos } = await findFromAddress()
101
+ formatProps.fromAddress = fromAddress
97
102
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
98
- const fee = getFee({ asset, feeData, utxos, compressed })
103
+ const { fee, sizeKB } = getFee({ asset, feeData, utxos, compressed })
99
104
 
100
105
  let amount = utxos.value.sub(fee)
101
106
  if (amount.isNegative) {
102
107
  throw new MoveFundsError('balance-negative', formatProps)
103
108
  }
104
109
 
105
- return { fromAddress, toAddress, amount, fee, utxos, privateKey }
106
- }
107
-
108
- const sendFunds = async (
109
- assetName,
110
- { fromAddress, toAddress, amount, fee, utxos, privateKey }
111
- ) => {
112
- assert(fromAddress, 'fromAddress is required')
113
- assert(toAddress, 'toAddress is required')
114
- assert(fee, 'fee is required')
115
- assert(utxos, 'utxos is required')
116
- assert(privateKey, 'privateKey is required')
117
- const selected = utxos
118
- const privateKeysAddressMap = {
119
- [fromAddress]: privateKey,
120
- }
121
110
  const unsignedTx = {
122
111
  txData: {
123
- inputs: createInputs(assetName, selected.toArray()),
112
+ inputs: createInputs(assetName, utxos.toArray()),
124
113
  outputs: [createOutput(assetName, toAddress, amount)],
125
114
  },
126
115
  txMeta: {
127
- addressPathsMap: selected.getAddressPathsMap(),
116
+ addressPathsMap: utxos.getAddressPathsMap(),
128
117
  },
129
118
  }
130
119
  const nonWitnessTxs = await getNonWitnessTxs(
131
120
  { name: assetName, address }, // pretty ugly hack!
132
- selected,
121
+ utxos,
133
122
  insightClient
134
123
  )
135
124
  Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
136
125
 
126
+ return { fromAddress, toAddress, amount, fee, utxos, unsignedTx, sizeKB, privateKey }
127
+ }
128
+
129
+ const sendFunds = async ({
130
+ assetName,
131
+ fromAddress,
132
+ toAddress,
133
+ amount,
134
+ fee,
135
+ unsignedTx,
136
+ privateKey,
137
+ }) => {
138
+ // the response from prepareSendFundsTx
139
+ assert(assetName, 'assetName is required')
140
+ assert(fromAddress, 'fromAddress is required')
141
+ assert(toAddress, 'toAddress is required')
142
+ assert(amount, 'amount is required')
143
+ assert(fee, 'fee is required')
144
+ assert(unsignedTx, 'unsignedTx is required')
145
+ assert(privateKey, 'privateKey is required')
146
+
147
+ const privateKeysAddressMap = {
148
+ [fromAddress]: privateKey,
149
+ }
150
+
137
151
  const { rawTx, txId } = await signTx({ unsignedTx, privateKeysAddressMap })
138
152
 
139
153
  await insightClient.broadcastTx(rawTx.toString('hex'))
@@ -158,7 +172,9 @@ export const moveFundsFactory = ({
158
172
  function getFee({ asset, feeData, utxos, compressed }) {
159
173
  const { feePerKB } = feeData
160
174
  const feeEstimator = getFeeEstimator(asset, feePerKB, { compressed })
161
- return feeEstimator({ inputs: utxos, outputs: [null] }).toDefault()
175
+ const fee = feeEstimator({ inputs: utxos, outputs: [null] })
176
+ const sizeKB = fee.toDefaultNumber() / feePerKB
177
+ return { fee, sizeKB }
162
178
  }
163
179
 
164
180
  function isValidPrivateKey(privateKey) {
@@ -171,7 +187,7 @@ export const moveFundsFactory = ({
171
187
  }
172
188
 
173
189
  return {
174
- prepareFunds,
190
+ prepareSendFundsTx,
175
191
  sendFunds,
176
192
  }
177
193
  }
@@ -16,6 +16,8 @@ import {
16
16
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
17
17
  import { getUsableUtxos, getUtxos } from '../utxos-utils'
18
18
 
19
+ import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
20
+
19
21
  const ASSETS_SUPPORTED_BIP_174 = [
20
22
  'bitcoin',
21
23
  'bitcoinregtest',
@@ -79,12 +81,40 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
79
81
  return rawTxs
80
82
  }
81
83
 
84
+ export const getSizeAndChangeScriptFactory = ({ bitcoinJsLib = defaultBitcoinjsLib } = {}) => ({
85
+ assetName,
86
+ tx,
87
+ rawTx,
88
+ changeUtxoIndex,
89
+ txId,
90
+ }) => {
91
+ assert(assetName, 'assetName is required')
92
+ assert(rawTx, 'rawTx is required')
93
+ assert(typeof changeUtxoIndex === 'number', 'changeUtxoIndex must be a number')
94
+ if (tx) {
95
+ return { script: tx.outs?.[changeUtxoIndex]?.script.toString('hex'), size: tx.virtualSize }
96
+ }
97
+ // Trezor doesn't return tx!! we need to reparse it!
98
+ const parsedTx = bitcoinJsLib.Transaction.fromBuffer(Buffer.from(rawTx, 'hex'))
99
+ try {
100
+ return {
101
+ script: parsedTx.outs?.[changeUtxoIndex]?.script.toString('hex'),
102
+ size: parsedTx.virtualSize(),
103
+ }
104
+ } catch (e) {
105
+ console.warn(
106
+ `tx-send warning: ${assetName} cannot extract script and size from tx ${txId}. ${e}`
107
+ )
108
+ return {}
109
+ }
110
+ }
111
+
82
112
  // not ported from Exodus; but this demos signing / broadcasting
83
113
  // NOTE: this will be ripped out in the coming weeks
84
114
 
85
115
  export const createAndBroadcastTXFactory = ({
86
116
  getFeeEstimator,
87
-
117
+ getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
88
118
  allowUnconfirmedRbfEnabledUtxos,
89
119
  }) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
90
120
  const {
@@ -308,6 +338,8 @@ export const createAndBroadcastTXFactory = ({
308
338
  }
309
339
  }
310
340
 
341
+ const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
342
+
311
343
  let remainingUtxos = usableUtxos.difference(selectedUtxos)
312
344
  if (changeUtxoIndex !== -1) {
313
345
  const address = Address.create(ourAddress.address, ourAddress.meta)
@@ -315,7 +347,7 @@ export const createAndBroadcastTXFactory = ({
315
347
  txId,
316
348
  address,
317
349
  vout: changeUtxoIndex,
318
- script: tx.outs[changeUtxoIndex].script.toString('hex'),
350
+ script,
319
351
  value: change,
320
352
  confirmations: 0,
321
353
  rbfEnabled,
@@ -364,9 +396,7 @@ export const createAndBroadcastTXFactory = ({
364
396
  data: {
365
397
  sent: selfSend ? [] : receivers,
366
398
  rbfEnabled,
367
- feePerKB: ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
368
- ? fee.div(tx.virtualSize / 1000).toBaseNumber()
369
- : undefined,
399
+ feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
370
400
  changeAddress: changeOutput ? ourAddress : undefined,
371
401
  blockHeight,
372
402
  blocksSeen: 0,
@@ -1,7 +1,10 @@
1
1
  import assert from 'minimalistic-assert'
2
2
  import lodash from 'lodash'
3
3
  import ECPairFactory from 'ecpair'
4
- import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
4
+ import { payments, Psbt, Transaction } from '@exodus/bitcoinjs-lib'
5
+ import { getOwnProperty } from '@exodus/basic-utils'
6
+
7
+ import secp256k1 from 'secp256k1'
5
8
 
6
9
  import { toAsyncSigner, tweakSigner } from './taproot'
7
10
 
@@ -45,10 +48,11 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
45
48
  ECPair = ECPair || ECPairFactory(ecc)
46
49
 
47
50
  const getKeyAndPurpose = lodash.memoize((address) => {
48
- // TODO: Consider using privateKeysAddressMap for other assets
49
51
  const purpose = resolvePurpose(address)
50
52
  if (privateKeysAddressMap) {
51
- const key = ECPair.fromWIF(privateKeysAddressMap[address], networkInfo)
53
+ const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
54
+ assert(privateKey, `there is no private key for address ${address}`)
55
+ const key = ECPair.fromWIF(privateKey, networkInfo)
52
56
  return { key, purpose }
53
57
  }
54
58
  const path = addressPathsMap[address]
@@ -96,6 +100,20 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
96
100
  for (let index = 0; index < inputs.length; index++) {
97
101
  const { address } = inputs[index]
98
102
  const { key, purpose } = getKeyAndPurpose(address)
103
+
104
+ if (purpose === 49) {
105
+ // If spending from a P2SH address, we assume the address is P2SH wrapping
106
+ // P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
107
+ // signing a P2SH input if we are importing a private key
108
+ // BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
109
+ const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
110
+ const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
111
+ const p2sh = payments.p2sh({ redeem: p2wpkh })
112
+ psbt.updateInput(index, {
113
+ redeemScript: p2sh.redeem.output,
114
+ })
115
+ }
116
+
99
117
  if (ecc.signSchnorrAsync) {
100
118
  // desktop / BE / mobile with bip-schnorr signing
101
119
  const isTaprootAddress = purpose === 86