@exodus/bitcoin-api 2.10.1 → 2.11.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.10.1",
3
+ "version": "2.11.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -57,5 +57,5 @@
57
57
  "jest-when": "^3.5.1",
58
58
  "safe-buffer": "^5.2.1"
59
59
  },
60
- "gitHead": "59489f1962adac06d14642120d7fe97e2a4f6d43"
60
+ "gitHead": "58f3aa71419faa3d2680bfbd65659cc9fcc5d223"
61
61
  }
@@ -11,7 +11,12 @@ export default class InsightWSClient extends EventEmitter {
11
11
 
12
12
  connect(addresses, opts) {
13
13
  const options = Object.assign(
14
- { transports: ['websocket'], reconnectionDelayMax: 30_000, reconnectionDelay: 10_000 },
14
+ {
15
+ transports: ['websocket'],
16
+ reconnectionDelayMax: 30_000,
17
+ reconnectionDelay: 10_000,
18
+ extraHeaders: { 'User-Agent': 'exodus' },
19
+ },
15
20
  opts
16
21
  )
17
22
 
@@ -128,16 +128,99 @@ export const getSizeAndChangeScriptFactory =
128
128
  }
129
129
  }
130
130
 
131
- // not ported from Exodus; but this demos signing / broadcasting
132
- // NOTE: this will be ripped out in the coming weeks
131
+ /**
132
+ * Signs a transaction using the provided asset client interface.
133
+ *
134
+ * @async
135
+ * @function signTransaction
136
+ * @param {Object} assetClientInterface - The asset client interface to use for signing the transaction.
137
+ * @param {string} assetName - The name of the asset.
138
+ * @param {Object} unsignedTx - The unsigned transaction to sign.
139
+ * @param {Object} walletAccount - The wallet account to use for signing.
140
+ * @returns {Promise<Object>} An object containing the signed raw transaction, transaction ID, and the signed transaction.
141
+ * @throws {Error} Throws an error if signing the transaction fails.
142
+ *
143
+ * @example
144
+ * // Example usage:
145
+ * const { rawTx, txId, tx } = await signTransaction({assetClientInterface, assetName, unsignedTx, walletAccount});
146
+ * // rawTx: Buffer data representing the signed raw transaction
147
+ * // txId: The ID of the signed transaction
148
+ * // tx: An object representing the signed transaction, containing virtualSize and outs
149
+ * // Example returned object:
150
+ * {
151
+ * rawTx: {
152
+ * type: "Buffer",
153
+ * data: [
154
+ * 2,
155
+ * ...
156
+ * ]
157
+ * },
158
+ * txId: "35244fba5cc46d6f3773689dce11ddba3f341149ef627c051e1a0bacd9458a1c",
159
+ * tx: {
160
+ * virtualSize: 110,
161
+ * outs: [
162
+ * {
163
+ * script: "0014c06da7c31e24ba4e7a0656443e17c00572a3e9f7"
164
+ * }
165
+ * ]
166
+ * }
167
+ * }
168
+ */
169
+ export async function signTransaction({
170
+ assetClientInterface,
171
+ assetName,
172
+ unsignedTx,
173
+ walletAccount,
174
+ }) {
175
+ const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
176
+ assetName,
177
+ unsignedTx,
178
+ walletAccount,
179
+ })
180
+ return { rawTx, txId, tx }
181
+ }
133
182
 
134
- export const createAndBroadcastTXFactory =
183
+ async function createUnsignedTx({
184
+ inputs,
185
+ outputs,
186
+ useCashAddress,
187
+ addressPathsMap,
188
+ blockHeight,
189
+ asset,
190
+ selectedUtxos,
191
+ insightClient,
192
+ }) {
193
+ const unsignedTx = {
194
+ txData: {
195
+ inputs,
196
+ outputs,
197
+ },
198
+ txMeta: {
199
+ useCashAddress, // for trezor to show the receiver cash address
200
+ addressPathsMap,
201
+ blockHeight,
202
+ },
203
+ }
204
+
205
+ const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
206
+ Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
207
+ return unsignedTx
208
+ }
209
+
210
+ async function getBlockHeight({ assetName, insightClient }) {
211
+ return ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
212
+ ? insightClient.fetchBlockHeight()
213
+ : 0
214
+ }
215
+
216
+ export const getPrepareSendTransaction =
135
217
  ({
218
+ blockHeight: providedBlockHeight,
219
+ ordinalsEnabled,
136
220
  getFeeEstimator,
137
- getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
138
221
  allowUnconfirmedRbfEnabledUtxos,
139
- ordinalsEnabled = false,
140
222
  utxosDescendingOrder,
223
+ rbfEnabled: providedRbfEnabled,
141
224
  }) =>
142
225
  async (
143
226
  { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
@@ -148,30 +231,33 @@ export const createAndBroadcastTXFactory =
148
231
  feePerKB,
149
232
  customFee,
150
233
  isSendAll,
151
- isExchange,
152
- isBip70,
153
234
  bumpTxId,
154
- isRbfAllowed = true,
155
235
  nft,
156
236
  feeOpts,
237
+ isExchange,
238
+ isBip70,
239
+ isRbfAllowed,
157
240
  } = options
158
241
 
242
+ const asset = maybeToken.baseAsset
243
+ const assetName = asset.name
244
+ const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
245
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
246
+ const insightClient = asset.baseAsset.insightClient
247
+
248
+ const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
159
249
  const brc20 = options.brc20 || feeOpts?.brc20 // feeOpts is the only way I've found atm to pass brc20 param without changing the tx-send hydra module
160
250
 
161
- const asset = maybeToken.baseAsset
251
+ const rbfEnabled =
252
+ providedRbfEnabled ||
253
+ (feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20)
162
254
 
163
255
  const isToken = maybeToken.name !== asset.name
164
-
165
256
  if (isToken) {
166
257
  assert(brc20, 'brc20 is required when sending bitcoin token')
167
258
  }
168
259
 
169
260
  const amount = isToken ? asset.currency.ZERO : tokenAmount
170
-
171
- const assetName = asset.name
172
-
173
- const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
174
-
175
261
  const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
176
262
 
177
263
  assert(
@@ -210,9 +296,8 @@ export const createAndBroadcastTXFactory =
210
296
  ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
211
297
  : undefined
212
298
 
213
- const insightClient = asset.baseAsset.insightClient
214
299
  const currency = asset.currency
215
- const feeData = await assetClientInterface.getFeeConfig({ assetName })
300
+
216
301
  const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
217
302
  const usableUtxos = getUsableUtxos({
218
303
  asset,
@@ -232,9 +317,6 @@ export const createAndBroadcastTXFactory =
232
317
  address = asset.address.P2SH2ToP2SH(address)
233
318
  }
234
319
 
235
- const rbfEnabled =
236
- feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
237
-
238
320
  let utxosToBump
239
321
  if (bumpTxId) {
240
322
  const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
@@ -250,7 +332,7 @@ export const createAndBroadcastTXFactory =
250
332
  }
251
333
  }
252
334
 
253
- const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
335
+ const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount
254
336
  const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
255
337
  const feeRate = feeData.feePerKB
256
338
  const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
@@ -301,18 +383,13 @@ export const createAndBroadcastTXFactory =
301
383
 
302
384
  const addressPathsMap = selectedUtxos.getAddressPathsMap()
303
385
 
304
- // transform UTXO object to raw
386
+ // Inputs and Outputs
305
387
  const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
388
+ let outputs = replaceTx
389
+ ? replaceTx.data.sent.map(({ address, amount }) => createOutput(assetName, address, amount))
390
+ : []
306
391
 
307
- let outputs
308
- if (replaceTx) {
309
- outputs = replaceTx.data.sent.map(({ address, amount }) =>
310
- createOutput(assetName, address, amount)
311
- )
312
- } else {
313
- outputs = []
314
- }
315
-
392
+ // Send output
316
393
  let sendOutput
317
394
  if (address) {
318
395
  if (transferOrdinalsUtxos) {
@@ -342,6 +419,7 @@ export const createAndBroadcastTXFactory =
342
419
  ourAddress = Address.create(legacyAddress, ourAddress.meta)
343
420
  }
344
421
 
422
+ // Change Output
345
423
  let changeOutput
346
424
  if (change.gte(dust)) {
347
425
  changeOutput = createOutput(assetName, ourAddress.address ?? ourAddress.toString(), change)
@@ -355,31 +433,111 @@ export const createAndBroadcastTXFactory =
355
433
  }
356
434
 
357
435
  outputs = replaceTx ? outputs : shuffle(outputs)
358
- const blockHeight = ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
359
- ? await insightClient.fetchBlockHeight()
360
- : 0
361
-
362
- // desktop/mobile shared format
363
- const unsignedTx = {
364
- txData: {
365
- inputs,
366
- outputs,
367
- },
368
- txMeta: {
369
- useCashAddress, // for trezor to show the receiver cash address
370
- addressPathsMap,
371
- blockHeight,
372
- },
436
+
437
+ const unsignedTx = await createUnsignedTx({
438
+ inputs,
439
+ outputs,
440
+ useCashAddress,
441
+ addressPathsMap,
442
+ blockHeight,
443
+ asset,
444
+ selectedUtxos,
445
+ insightClient,
446
+ })
447
+ return {
448
+ amount,
449
+ change,
450
+ totalAmount,
451
+ currentOrdinalsUtxos,
452
+ inscriptionIds,
453
+ address,
454
+ ourAddress,
455
+ receiveAddress,
456
+ sendAmount,
457
+ fee,
458
+ usableUtxos,
459
+ selectedUtxos,
460
+ transferOrdinalsUtxos,
461
+ replaceTx,
462
+ sendOutput,
463
+ changeOutput,
464
+ unsignedTx,
373
465
  }
466
+ }
467
+
468
+ // not ported from Exodus; but this demos signing / broadcasting
469
+ // NOTE: this will be ripped out in the coming weeks
470
+ export const createAndBroadcastTXFactory =
471
+ ({
472
+ getFeeEstimator,
473
+ getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
474
+ allowUnconfirmedRbfEnabledUtxos,
475
+ ordinalsEnabled = false,
476
+ utxosDescendingOrder,
477
+ }) =>
478
+ async (
479
+ { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
480
+ { assetClientInterface }
481
+ ) => {
482
+ // Prepare transaction
483
+ const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true, feeOpts } = options
374
484
 
375
- const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
376
- Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
377
- const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
485
+ const asset = maybeToken.baseAsset
486
+ const assetName = asset.name
487
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
488
+ const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
489
+ const insightClient = asset.baseAsset.insightClient
490
+ const isToken = maybeToken.name !== asset.name
491
+ const brc20 = options.brc20 || feeOpts?.brc20
492
+
493
+ const rbfEnabled =
494
+ feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
495
+
496
+ // blockHeight
497
+ const blockHeight = await getBlockHeight({ assetName, insightClient })
498
+
499
+ const transactionDescriptor = await getPrepareSendTransaction({
500
+ blockHeight,
501
+ ordinalsEnabled,
502
+ getFeeEstimator,
503
+ allowUnconfirmedRbfEnabledUtxos,
504
+ utxosDescendingOrder,
505
+ rbfEnabled,
506
+ })(
507
+ { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
508
+ { assetClientInterface }
509
+ )
510
+ const {
511
+ amount,
512
+ change,
513
+ totalAmount,
514
+ currentOrdinalsUtxos,
515
+ inscriptionIds,
516
+ ourAddress,
517
+ receiveAddress,
518
+ sendAmount,
519
+ fee,
520
+ usableUtxos,
521
+ selectedUtxos,
522
+ transferOrdinalsUtxos,
523
+ replaceTx,
524
+ sendOutput,
525
+ changeOutput,
526
+ unsignedTx,
527
+ } = transactionDescriptor
528
+ const outputs = unsignedTx.txData.outputs
529
+
530
+ address = transactionDescriptor.address
531
+
532
+ // Sign transaction
533
+ const { rawTx, txId, tx } = await signTransaction({
534
+ assetClientInterface,
378
535
  assetName,
379
536
  unsignedTx,
380
537
  walletAccount,
381
538
  })
382
539
 
540
+ // Broadcast transaction
383
541
  const broadcastTxWithRetry = retry(
384
542
  async (rawTx) => {
385
543
  try {
@@ -8,7 +8,6 @@ import secp256k1 from 'secp256k1'
8
8
  const ECPair = getECPair()
9
9
 
10
10
  export const createGetKeyAndPurpose = ({
11
- keys,
12
11
  hdkeys,
13
12
  resolvePurpose,
14
13
  addressPathsMap,
@@ -22,7 +21,7 @@ export const createGetKeyAndPurpose = ({
22
21
  return getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address)
23
22
  }
24
23
 
25
- return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address)
24
+ return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, networkInfo, purpose, address)
26
25
  })
27
26
 
28
27
  function getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address) {
@@ -33,15 +32,14 @@ function getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, addre
33
32
  return { key, purpose, publicKey }
34
33
  }
35
34
 
36
- function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address) {
35
+ function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, networkInfo, purpose, address) {
37
36
  const path = getOwnProperty(addressPathsMap, address, 'string')
38
37
  assert(hdkeys, 'hdkeys must be provided')
39
38
  assert(purpose, `purpose for address ${address} could not be resolved`)
40
39
  const hdkey = hdkeys[purpose]
41
40
  assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
42
41
  const derivedhdkey = hdkey.derive(path)
43
- const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
44
- const key = ECPair.fromWIF(privateEncoded, networkInfo)
42
+ const key = ECPair.fromPrivateKey(derivedhdkey.privateKey, { network: networkInfo })
45
43
  const publicKey = derivedhdkey.publicKey
46
44
  return { key, publicKey, purpose }
47
45
  }
@@ -1,14 +1,10 @@
1
1
  import { Transaction, payments } from '@exodus/bitcoinjs-lib'
2
2
 
3
- import { getECPair } from '../bitcoinjs-lib'
4
3
  import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
5
4
  import { createGetKeyAndPurpose } from './create-get-key-and-purpose'
6
- import { toAsyncSigner, tweakSigner } from './taproot'
7
-
8
- const ECPair = getECPair()
5
+ import { toAsyncSigner } from './taproot'
9
6
 
10
7
  export function createSignWithWallet({
11
- keys,
12
8
  hdkeys,
13
9
  resolvePurpose,
14
10
  privateKeysAddressMap,
@@ -17,7 +13,6 @@ export function createSignWithWallet({
17
13
  network,
18
14
  }) {
19
15
  const getKeyAndPurpose = createGetKeyAndPurpose({
20
- keys,
21
16
  hdkeys,
22
17
  resolvePurpose,
23
18
  privateKeysAddressMap,
@@ -70,8 +65,11 @@ export function createSignWithWallet({
70
65
  }
71
66
 
72
67
  // desktop / BE / mobile with bip-schnorr signing
73
- const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
74
- await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), allowedSigHashTypes)
68
+ await psbt.signInputAsync(
69
+ index,
70
+ toAsyncSigner({ keyPair: key, isTaprootAddress, network }),
71
+ allowedSigHashTypes
72
+ )
75
73
  }
76
74
  }
77
75
  }
@@ -4,10 +4,9 @@ import { createPrepareForSigning } from './default-prepare-for-signing'
4
4
  import { createSignWithWallet } from './create-sign-with-wallet'
5
5
  import { extractTransaction } from './common'
6
6
 
7
- export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network }) => {
7
+ export const signTxFactory = ({ assetName, resolvePurpose, coinInfo, network }) => {
8
8
  assert(assetName, 'assetName is required')
9
9
  assert(resolvePurpose, 'resolvePurpose is required')
10
- assert(keys, 'keys is required')
11
10
  assert(coinInfo, 'coinInfo is required')
12
11
 
13
12
  const prepareForSigning = createPrepareForSigning({
@@ -26,7 +25,6 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
26
25
 
27
26
  const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
28
27
  const signWithWallet = createSignWithWallet({
29
- keys,
30
28
  hdkeys,
31
29
  resolvePurpose,
32
30
  privateKeysAddressMap,
@@ -8,7 +8,7 @@ import { getECPair } from '../bitcoinjs-lib'
8
8
  const ecc = eccFactory()
9
9
  const ECPair = getECPair()
10
10
 
11
- export function tweakSigner({ signer, tweakHash, network }) {
11
+ function tweakSigner({ signer, tweakHash, network }) {
12
12
  assert(signer, 'signer is required')
13
13
 
14
14
  let privateKey = signer.privateKey
@@ -40,8 +40,13 @@ function tapTweakHash(pubKey, h) {
40
40
  /**
41
41
  * Take a sync signer and make it async.
42
42
  */
43
- export function toAsyncSigner({ keyPair }) {
43
+ export function toAsyncSigner({ keyPair, isTaprootAddress, network }) {
44
44
  assert(keyPair, 'keyPair is required')
45
+
46
+ if (isTaprootAddress) {
47
+ keyPair = tweakSigner({ signer: keyPair, ECPair, network })
48
+ }
49
+
45
50
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
46
51
  keyPair.sign = async (h) => {
47
52
  const sig = await ecc.signAsync(h, keyPair.privateKey)