@exodus/bitcoin-api 2.20.0 → 2.21.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,28 @@
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.21.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.20.1...@exodus/bitcoin-api@2.21.0) (2024-07-19)
7
+
8
+
9
+ ### Features
10
+
11
+ * **BTC:** create batch tx from array of recipients ([#2881](https://github.com/ExodusMovement/assets/issues/2881)) ([440d5d5](https://github.com/ExodusMovement/assets/commit/440d5d5879c902ef10ff7644c92a5092bb868872))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **bitcoin-api:** stop ws connection on monitor stop ([#2851](https://github.com/ExodusMovement/assets/issues/2851)) ([e52f4c2](https://github.com/ExodusMovement/assets/commit/e52f4c23e7635cc3116c876ad35fbbab4a70c32b))
17
+
18
+
19
+
20
+ ## [2.20.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.20.0...@exodus/bitcoin-api@2.20.1) (2024-07-15)
21
+
22
+ **Note:** Version bump only for package @exodus/bitcoin-api
23
+
24
+
25
+
26
+
27
+
6
28
  ## [2.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.19.0...@exodus/bitcoin-api@2.20.0) (2024-07-09)
7
29
 
8
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  "access": "restricted"
15
15
  },
16
16
  "scripts": {
17
- "test": "run -T jest",
17
+ "test": "run -T exodus-test --jest --esbuild",
18
18
  "lint": "run -T eslint .",
19
19
  "lint:fix": "yarn lint --fix"
20
20
  },
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "0dc141fc6a249ac051a5ca591d46cbc81096a4f3"
69
+ "gitHead": "ba7439e1b8ae182a2a95ffd0ea09b8bc3330f2f6"
70
70
  }
@@ -1,6 +1,11 @@
1
1
  import { AccountState, UtxoCollection } from '@exodus/models'
2
2
 
3
- export function createAccountState({ asset, ordinalsEnabled = false, brc20Enabled = false }) {
3
+ export function createAccountState({
4
+ asset,
5
+ ordinalsEnabled = false,
6
+ brc20Enabled = false,
7
+ isMagicEdenFungibleBalancesEnabled = false,
8
+ }) {
4
9
  const empty = UtxoCollection.createEmpty({
5
10
  currency: asset.currency,
6
11
  })
@@ -22,6 +27,10 @@ export function createAccountState({ asset, ordinalsEnabled = false, brc20Enable
22
27
  defaults.brc20Balances = {}
23
28
  }
24
29
 
30
+ if (isMagicEdenFungibleBalancesEnabled) {
31
+ defaults.magicEdenApiFungibleBalances = []
32
+ }
33
+
25
34
  return class BitcoinAccountState extends AccountState {
26
35
  static defaults = defaults
27
36
  }
package/src/balances.js CHANGED
@@ -13,6 +13,20 @@ export const getBalancesFactory = ({ feeData, getSpendableBalance, ordinalsEnabl
13
13
  assert(asset, 'asset is required')
14
14
  assert(accountState, 'accountState is required')
15
15
  assert(txLog, 'txLog is required')
16
+
17
+ if (accountState.magicEdenApiFungibleBalances) {
18
+ const validBalances = accountState.magicEdenApiFungibleBalances.filter(
19
+ (fungibleBalance) => fungibleBalance.asset.ticker === asset.ticker
20
+ )
21
+ const parsedBalances = validBalances.map(({ balance }) =>
22
+ asset.currency.defaultUnit(balance.balance)
23
+ )
24
+ const totalBalance = asset.currency.defaultUnit(
25
+ parsedBalances.reduce((sum, item) => sum.add(item), asset.currency.ZERO)
26
+ )
27
+ return { balance: totalBalance }
28
+ }
29
+
16
30
  const utxos = getUtxos({ asset, accountState })
17
31
  const balance = utxos.value
18
32
  const spendableBalance = getSpendableBalance({
@@ -4,7 +4,7 @@ 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'
7
- import * as secp256k1 from 'secp256k1'
7
+ import secp256k1 from 'secp256k1'
8
8
  import { hash160 } from './hash-utils'
9
9
  import { toXOnly } from './bitcoinjs-lib/ecc-utils'
10
10
  import { ecc } from './bitcoinjs-lib/ecc'
@@ -27,6 +27,7 @@ export const selectUtxos = ({
27
27
  amount,
28
28
  feeRate,
29
29
  receiveAddress, // it could be null
30
+ receiveAddresses = [],
30
31
  isSendAll,
31
32
  getFeeEstimator,
32
33
  disableReplacement = false,
@@ -44,10 +45,9 @@ export const selectUtxos = ({
44
45
  inscriptionIds,
45
46
  })
46
47
 
47
- const receiveAddresses = []
48
48
  if (inscriptionIds) {
49
49
  receiveAddresses.push(...inscriptionIds.map(() => resolvedReceiveAddresses))
50
- } else {
50
+ } else if (receiveAddresses.length === 0) {
51
51
  receiveAddresses.push(resolvedReceiveAddresses)
52
52
  }
53
53
 
package/src/index.js CHANGED
@@ -14,6 +14,8 @@ export * from './utxos-utils'
14
14
  export * from './tx-log'
15
15
  export * from './unconfirmed-ancestor-data'
16
16
  export * from './parse-unsigned-tx'
17
+ export { getCreateBatchTransaction } from './tx-send/batch-tx'
18
+ export { createPsbtToUnsignedTx } from './psbt-utils'
17
19
  export * from './insight-api-client/util'
18
20
  export * from './move-funds'
19
21
  export { createEncodeMultisigContract } from './multisig-address'
package/src/move-funds.js CHANGED
@@ -2,7 +2,7 @@ import wif from 'wif'
2
2
  import { UtxoCollection, Address } from '@exodus/models'
3
3
  import { createInputs, createOutput, getNonWitnessTxs } from './tx-send'
4
4
  import assert from 'minimalistic-assert'
5
- import * as secp256k1 from 'secp256k1'
5
+ import secp256k1 from 'secp256k1'
6
6
 
7
7
  const isValidPrivateKey = (privateKey) => {
8
8
  try {
@@ -0,0 +1,61 @@
1
+ import BipPath from 'bip32-path'
2
+ import lodash from 'lodash'
3
+
4
+ export const createPsbtToUnsignedTx =
5
+ ({ assetClientInterface, assetName }) =>
6
+ async ({ psbt, walletAccount, purpose = 86 }) => {
7
+ const addressPathsMap = {}
8
+ const inputsToSign = []
9
+
10
+ const addressOpts = {
11
+ walletAccount: walletAccount.toString(),
12
+ assetName,
13
+ purpose,
14
+ chainIndex: 0,
15
+ addressIndex: 0,
16
+ }
17
+
18
+ // Need to have all input derivations
19
+ for (const i of lodash.range(psbt.inputCount)) {
20
+ const input = psbt.data.inputs[i]
21
+
22
+ const derivation = input.tapBip32Derivation
23
+ if (!derivation) {
24
+ throw new Error('Invalid input in psbt, no derivation for input found')
25
+ }
26
+
27
+ const [chainIndex, addressIndex] = BipPath.fromString(derivation[0].path).toPathArray()
28
+
29
+ addressOpts.chainIndex = chainIndex
30
+ addressOpts.addressIndex = addressIndex
31
+
32
+ const address = await assetClientInterface.getAddress(addressOpts)
33
+
34
+ addressPathsMap[address.toString()] = derivation[0].path
35
+ inputsToSign.push({ address: address.toString() })
36
+ }
37
+
38
+ // If we have output derivations then it's our change
39
+ for (const i of lodash.range(psbt.txOutputs.length)) {
40
+ const output = psbt.data.outputs[i]
41
+
42
+ const derivation = output.tapBip32Derivation
43
+ if (!derivation) continue
44
+ const [chainIndex, addressIndex] = BipPath.fromString(derivation[0].path).toPathArray()
45
+
46
+ addressOpts.chainIndex = chainIndex
47
+ addressOpts.addressIndex = addressIndex
48
+ const address = await assetClientInterface.getAddress(addressOpts)
49
+
50
+ addressPathsMap[address.toString()] = derivation[0].path
51
+ }
52
+
53
+ return {
54
+ txData: { psbtBuffer: psbt.toBuffer() },
55
+ txMeta: {
56
+ addressPathsMap,
57
+ inputsToSign,
58
+ accountIndex: walletAccount.index,
59
+ },
60
+ }
61
+ }
@@ -0,0 +1,78 @@
1
+ import { Monitor as BitcoinMonitor } from './bitcoin-monitor'
2
+ import fetchFlagrEvaluation from './me-flagr'
3
+
4
+ const FLAGR_KEY = 'bitcoin-balance-api'
5
+
6
+ export class MagicEdenBitcoinMonitor extends BitcoinMonitor {
7
+ constructor(args) {
8
+ super(args)
9
+ this.useMagicEdenMonitor = false
10
+ }
11
+
12
+ async setServer(assetConfig = {}) {
13
+ super.setServer(assetConfig)
14
+
15
+ const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
16
+ const address = await this.aci.getReceiveAddress({
17
+ assetName: this.asset.name,
18
+ walletAccount: walletAccounts[0],
19
+ useCache: true,
20
+ })
21
+ await this.initFlagr(address)
22
+ }
23
+
24
+ async initFlagr(address) {
25
+ if (this.useMagicEdenMonitor) return
26
+ try {
27
+ const response = await fetchFlagrEvaluation(address, FLAGR_KEY)
28
+ if (response.variantKey === 'on') {
29
+ this.useMagicEdenMonitor = true
30
+ }
31
+ } catch (error) {
32
+ console.error('Failed to fetch useMagicEdenMonitor config:', error)
33
+ }
34
+ }
35
+
36
+ async fetchFungibleBalances(walletAccount) {
37
+ if (!this.useMagicEdenMonitor) return
38
+
39
+ const purposes = await this.aci.getSupportedPurposes({
40
+ assetName: this.asset.name,
41
+ walletAccount,
42
+ })
43
+
44
+ const addresses = await Promise.all(
45
+ purposes.map((purpose) =>
46
+ this.aci.getReceiveAddress({
47
+ assetName: this.asset.name,
48
+ walletAccount,
49
+ useCache: true,
50
+ purpose,
51
+ })
52
+ )
53
+ )
54
+
55
+ const response = await fetch('https://api-mainnet.magiceden.io/v1/wallet/balances/fungible', {
56
+ method: 'POST',
57
+ headers: {
58
+ Accept: 'application/json',
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(
62
+ addresses.map((address) => ({ address, chain: this.asset.baseAssetName }))
63
+ ),
64
+ })
65
+
66
+ if (!response.ok) {
67
+ throw new Error(
68
+ `Unable to fetch bitcoin fungible balanes: API returned ${response.status}: ${response.statusText || 'Unknown Status Text'}`
69
+ )
70
+ }
71
+
72
+ const { balances } = await response.json()
73
+
74
+ return balances
75
+ }
76
+ }
77
+
78
+ export const createMagicEdenBitcoinMonitor = (args) => new MagicEdenBitcoinMonitor(args)
@@ -62,6 +62,12 @@ export class Monitor extends BaseMonitor {
62
62
  })
63
63
 
64
64
  this.addHook('after-tick-multiple-wallet-accounts', () => this.#subscribeToNewAddresses())
65
+ this.addHook('before-stop', () => {
66
+ if (this.#ws) {
67
+ this.#ws.close()
68
+ this.#ws = null
69
+ }
70
+ })
65
71
  this.addHook('after-stop', async () =>
66
72
  Promise.all(Object.keys(this.#runningByWalletAccount).map(this.#waitForWalletToFinish))
67
73
  )
@@ -281,6 +287,12 @@ export class Monitor extends BaseMonitor {
281
287
  newData.mem = { unconfirmedTxAncestor }
282
288
  }
283
289
 
290
+ if (this.fetchFungibleBalances) {
291
+ try {
292
+ newData.magicEdenApiFungibleBalances = await this.fetchFungibleBalances(walletAccount)
293
+ } catch {}
294
+ }
295
+
284
296
  await aci.updateAccountState({
285
297
  assetName,
286
298
  walletAccount,
@@ -1,2 +1,3 @@
1
1
  export * from './bitcoin-monitor'
2
+ export * from './magic-eden-bitcoin-monitor'
2
3
  export * from './bitcoin-monitor-scanner'
@@ -0,0 +1,86 @@
1
+ import { Monitor as BitcoinMonitor } from './bitcoin-monitor'
2
+ import fetchFlagrEvaluation from './me-flagr'
3
+
4
+ const FLAGR_KEY = 'bitcoin-balance-api'
5
+
6
+ export class MagicEdenBitcoinMonitor extends BitcoinMonitor {
7
+ constructor(args) {
8
+ super(args)
9
+ this.useMagicEdenMonitor = false
10
+ }
11
+
12
+ async setServer(assetConfig = {}) {
13
+ super.setServer(assetConfig)
14
+
15
+ const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
16
+ const address = await this.aci.getReceiveAddress({
17
+ assetName: this.asset.name,
18
+ walletAccount: walletAccounts[0],
19
+ useCache: true,
20
+ })
21
+ await this.initFlagr(address)
22
+ }
23
+
24
+ async initFlagr(address) {
25
+ if (this.useMagicEdenMonitor) return
26
+ try {
27
+ const response = await fetchFlagrEvaluation(address, FLAGR_KEY)
28
+ if (response.variantKey === 'on') {
29
+ this.useMagicEdenMonitor = true
30
+ }
31
+ } catch (error) {
32
+ console.error('Failed to fetch useMagicEdenMonitor config:', error)
33
+ }
34
+ }
35
+
36
+ async fetchFungibleBalances(walletAccount) {
37
+ if (!this.useMagicEdenMonitor) return
38
+
39
+ const purposes = await this.aci.getSupportedPurposes({
40
+ assetName: this.asset.name,
41
+ walletAccount,
42
+ })
43
+
44
+ const addresses = await Promise.all(
45
+ purposes.map((purpose) =>
46
+ this.aci.getReceiveAddress({
47
+ assetName: this.asset.name,
48
+ walletAccount,
49
+ useCache: true,
50
+ purpose,
51
+ })
52
+ )
53
+ )
54
+
55
+ const response = await fetch('https://api-mainnet.magiceden.io/v1/wallet/balances/fungible', {
56
+ method: 'POST',
57
+ headers: {
58
+ Accept: 'application/json',
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(
62
+ addresses.map((address) => ({ address, chain: this.asset.baseAssetName }))
63
+ ),
64
+ })
65
+
66
+ if (!response.ok) {
67
+ throw new Error(
68
+ `Unable to fetch bitcoin fungible balanes: API returned ${response.status}: ${response.statusText || 'Unknown Status Text'}`
69
+ )
70
+ }
71
+
72
+ const { balances } = await response.json()
73
+
74
+ const metadata = new Map()
75
+ balances.forEach((balance) => {
76
+ if (balance.asset?.id && balance.image) {
77
+ metadata.set(balance.asset.id, { imageURL: balance.image })
78
+ }
79
+ })
80
+ this.emit('token-metadata', { source: 'bitcoin', metadata })
81
+
82
+ return balances
83
+ }
84
+ }
85
+
86
+ export const createMagicEdenBitcoinMonitor = (args) => new MagicEdenBitcoinMonitor(args)
@@ -0,0 +1,22 @@
1
+ const FLAGR_URL = 'https://flagr-w.magiceden.io/api/v1/evaluation'
2
+
3
+ const fetchFlagrEvaluation = async (wallet, flagKey) => {
4
+ const response = await fetch(FLAGR_URL, {
5
+ method: 'POST',
6
+ headers: {
7
+ Accept: 'application/json',
8
+ 'Content-Type': 'application/json',
9
+ },
10
+ body: JSON.stringify({ entityContext: { wallet }, flagKey }),
11
+ })
12
+
13
+ if (!response.ok) {
14
+ throw new Error(
15
+ `${FLAGR_URL} returned ${response.status}: ${response.statusText || 'Unknown Status Text'}`
16
+ )
17
+ }
18
+
19
+ return response.json()
20
+ }
21
+
22
+ export default fetchFlagrEvaluation
@@ -0,0 +1,206 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import BIP32 from '@exodus/bip32'
3
+ import { Psbt } from '@exodus/bitcoinjs-lib'
4
+ import BipPath from 'bip32-path'
5
+ // Using this notation so it can be mocked by jest
6
+ import doShuffle from 'lodash/shuffle'
7
+ import assert from 'minimalistic-assert'
8
+
9
+ import { selectUtxos } from '../fee/utxo-selector'
10
+ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
11
+ import { getUsableUtxos, getUtxos } from '../utxos-utils'
12
+
13
+ const DUST_VALUES = {
14
+ P2WPKH: 294,
15
+ P2TR: 330,
16
+ }
17
+
18
+ export const getCreateBatchTransaction = ({
19
+ getFeeEstimator,
20
+ assetClientInterface,
21
+ changeAddressType,
22
+ }) => {
23
+ assert(assetClientInterface, `assetClientInterface must be supplied in sendTx`)
24
+
25
+ return async ({ assetName, walletAccount, recipients, options = {} }) => {
26
+ const stuff = await assetClientInterface.getAssetsForNetwork({ baseAssetName: assetName })
27
+ const asset = stuff[assetName]
28
+
29
+ const {
30
+ feeData = await assetClientInterface.getFeeConfig({ assetName }),
31
+ taprootInputWitnessSize,
32
+ } = options
33
+
34
+ const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
35
+
36
+ const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
37
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
38
+ const usableUtxos = getUsableUtxos({
39
+ asset,
40
+ utxos: getUtxos({ accountState, asset }),
41
+ feeData,
42
+ txSet,
43
+ unconfirmedTxAncestor,
44
+ })
45
+
46
+ const amount = recipients.reduce((acc, curr) => acc.add(curr.amount), asset.currency.ZERO)
47
+ const receiveAddresses = recipients.map((recipient) => recipient.address)
48
+
49
+ const { selectedUtxos, fee } = selectUtxos({
50
+ asset,
51
+ usableUtxos,
52
+ amount,
53
+ feeRate: feeData.feePerKB,
54
+ receiveAddresses,
55
+ getFeeEstimator: (asset, { feePerKB, ...options }) =>
56
+ getFeeEstimator(asset, feePerKB, options),
57
+ unconfirmedTxAncestor,
58
+ taprootInputWitnessSize,
59
+ changeAddressType,
60
+ allowUnconfirmedRbfEnabledUtxos: false,
61
+ })
62
+
63
+ if (!selectedUtxos) throw new Error('Not enough funds.')
64
+
65
+ const addressPathsMap = selectedUtxos.getAddressPathsMap()
66
+
67
+ const psbt = new Psbt({ network: asset.coinInfo.toBitcoinJS() })
68
+
69
+ for (const utxo of doShuffle(selectedUtxos.toArray())) {
70
+ const path = addressPathsMap[utxo.address]
71
+ if (!path) {
72
+ throw new Error(`Path missing for input address ${utxo.address}`)
73
+ }
74
+
75
+ const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
76
+ const addressOpts = {
77
+ walletAccount,
78
+ assetName,
79
+ chainIndex: 0,
80
+ addressIndex: 0,
81
+ }
82
+ addressOpts.chainIndex = chainIndex
83
+ addressOpts.addressIndex = addressIndex
84
+ addressOpts.purpose = utxo.address.meta.purpose
85
+
86
+ const [address, xpub] = await Promise.all([
87
+ assetClientInterface.getAddress(addressOpts),
88
+ assetClientInterface.getExtendedPublicKey(addressOpts),
89
+ ])
90
+ assert(String(address) === String(utxo.address))
91
+
92
+ const hdkey = BIP32.fromXPub(xpub)
93
+ const masterFingerprint = Buffer.alloc(4)
94
+ masterFingerprint.writeUint32BE(hdkey.fingerprint)
95
+
96
+ const input = {
97
+ hash: utxo.txId,
98
+ index: utxo.vout,
99
+ witnessUtxo: {
100
+ value: parseInt(utxo.value.toBaseString(), 10),
101
+ script: Buffer.from(utxo.script, 'hex'),
102
+ },
103
+ }
104
+
105
+ if (address.meta.spendingInfo) {
106
+ const { witness, redeem } = address.meta.spendingInfo
107
+ input.tapLeafScript = [
108
+ {
109
+ leafVersion: redeem.redeemVersion,
110
+ script: redeem.output,
111
+ controlBlock: witness[witness.length - 1],
112
+ },
113
+ ]
114
+ }
115
+
116
+ psbt.addInput(input)
117
+
118
+ const pubkey = hdkey.derive(path).publicKey.slice(1)
119
+ const index = psbt.data.inputs.length - 1
120
+ psbt.data.inputs[index].tapBip32Derivation = [
121
+ {
122
+ path,
123
+ leafHashes: [],
124
+ masterFingerprint,
125
+ pubkey,
126
+ },
127
+ ]
128
+ }
129
+
130
+ const change = selectedUtxos.value.sub(amount).sub(fee)
131
+ if (change.gte(asset.currency.baseUnit(DUST_VALUES[changeAddressType]))) {
132
+ const changeAddress = await assetClientInterface.getNextChangeAddress({
133
+ assetName,
134
+ walletAccount,
135
+ })
136
+
137
+ const output = { address: String(changeAddress), amount: change }
138
+
139
+ const path = changeAddress.meta.path
140
+
141
+ const xpub = await assetClientInterface.getExtendedPublicKey({ walletAccount, assetName })
142
+ const hdkey = BIP32.fromXPub(xpub)
143
+ const masterFingerprint = Buffer.alloc(4)
144
+ masterFingerprint.writeUint32BE(hdkey.fingerprint)
145
+
146
+ const pubkey = hdkey.derive(path).publicKey.slice(1)
147
+ output.tapBip32Derivation = [
148
+ {
149
+ path,
150
+ leafHashes: [],
151
+ masterFingerprint,
152
+ pubkey,
153
+ },
154
+ ]
155
+
156
+ recipients.push(output)
157
+ }
158
+
159
+ for (const recipient of doShuffle(recipients)) {
160
+ psbt.addOutput({
161
+ address: recipient.address,
162
+ value: parseInt(recipient.amount.toBaseString(), 10),
163
+ unknownKeyVals: [],
164
+ })
165
+
166
+ const index = psbt.data.outputs.length - 1
167
+ if (recipient.tapBip32Derivation) {
168
+ psbt.data.outputs[index].tapBip32Derivation = recipient.tapBip32Derivation
169
+ }
170
+
171
+ if (recipient.name) {
172
+ psbt.data.outputs[index].unknownKeyVals.push({
173
+ key: Buffer.from('name', 'utf8'),
174
+ value: Buffer.from(recipient.name, 'utf8'),
175
+ })
176
+ }
177
+
178
+ if (recipient.email) {
179
+ psbt.data.outputs[index].unknownKeyVals.push({
180
+ key: Buffer.from('email', 'utf8'),
181
+ value: Buffer.from(recipient.email, 'utf8'),
182
+ })
183
+ }
184
+
185
+ if (recipient.description) {
186
+ psbt.data.outputs[index].unknownKeyVals.push({
187
+ key: Buffer.from('description', 'utf8'),
188
+ value: Buffer.from(recipient.description, 'utf8'),
189
+ })
190
+ }
191
+
192
+ if (recipient.fiatAmount) {
193
+ psbt.data.outputs[index].unknownKeyVals.push({
194
+ key: Buffer.from('fiatAmount', 'utf8'),
195
+ value: Buffer.from(recipient.fiatAmount.toDefaultString(), 'utf8'),
196
+ })
197
+ }
198
+ }
199
+
200
+ const blockHeight = await asset.baseAsset.insightClient.fetchBlockHeight()
201
+
202
+ psbt.setLocktime(blockHeight)
203
+
204
+ return psbt
205
+ }
206
+ }
@@ -1,6 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
- // Using this notation so it can be mocked by jest
3
- import doShuffle from 'lodash/shuffle'
2
+ import lodash from 'lodash'
4
3
 
5
4
  import { UtxoCollection, Address } from '@exodus/models'
6
5
  import { retry } from '@exodus/simple-retry'
@@ -268,7 +267,8 @@ export const getPrepareSendTransaction =
268
267
  )
269
268
 
270
269
  const shuffle = (list) => {
271
- return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
270
+ // Using full lodash.shuffle notation so it can be mocked with spyOn in tests
271
+ return inscriptionIds ? list : lodash.shuffle(list) // don't shuffle when sending ordinal!!!!
272
272
  }
273
273
 
274
274
  assert(
@@ -335,7 +335,11 @@ export const getPrepareSendTransaction =
335
335
  }
336
336
 
337
337
  const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount
338
- const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
338
+ const receiveAddress = bumpTxId
339
+ ? replaceableTxs.length > 0
340
+ ? null
341
+ : changeAddressType
342
+ : address
339
343
  const feeRate = feeData.feePerKB
340
344
  const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
341
345
 
@@ -6,7 +6,7 @@ import KeyIdentifier from '@exodus/key-identifier'
6
6
 
7
7
  import { getECPair } from '../bitcoinjs-lib'
8
8
 
9
- import * as secp256k1 from 'secp256k1'
9
+ import secp256k1 from 'secp256k1'
10
10
 
11
11
  const ECPair = getECPair()
12
12
 
@@ -0,0 +1,6 @@
1
+ // extension point so tests can assert signatures deterministically
2
+ // this has to be CJS to still be mockable once we migrate to ESM world
3
+
4
+ function getSchnorrEntropy() {}
5
+
6
+ module.exports = { getSchnorrEntropy }
@@ -1,7 +1,7 @@
1
1
  import { crypto } from '@exodus/bitcoinjs-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
- import { getSchnorrEntropy } from './default-entropy'
4
+ import defaultEntropy from './default-entropy.cjs'
5
5
  import { ecc } from '../bitcoinjs-lib/ecc'
6
6
  import { getECPair } from '../bitcoinjs-lib'
7
7
 
@@ -59,7 +59,8 @@ export function toAsyncSigner({ keyPair, isTaprootKeySpend, network }) {
59
59
 
60
60
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
61
61
  keyPair.signSchnorr = async (h) => {
62
- const sig = ecc.signSchnorr(h, keyPair.privateKey, getSchnorrEntropy())
62
+ // defaultEntropy.getSchnorrEntropy() is mockable with jest.spyOn
63
+ const sig = ecc.signSchnorr(h, keyPair.privateKey, defaultEntropy.getSchnorrEntropy())
63
64
  return Buffer.from(sig)
64
65
  }
65
66
 
@@ -94,7 +95,8 @@ export async function toAsyncBufferSigner({ signer, purpose, keyId, isTaprootKey
94
95
  keyId,
95
96
  signatureType: 'schnorr',
96
97
  tweak,
97
- extraEntropy: getSchnorrEntropy(),
98
+ // defaultEntropy.getSchnorrEntropy() is mockable with jest.spyOn
99
+ extraEntropy: defaultEntropy.getSchnorrEntropy(),
98
100
  })
99
101
  },
100
102
  publicKey,
@@ -1,2 +0,0 @@
1
- // extension point so tests can assert signatures deterministically
2
- export function getSchnorrEntropy() {}