@exodus/bitcoin-api 2.19.0 → 2.20.1

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,23 @@
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.20.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.20.0...@exodus/bitcoin-api@2.20.1) (2024-07-15)
7
+
8
+ **Note:** Version bump only for package @exodus/bitcoin-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [2.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.19.0...@exodus/bitcoin-api@2.20.0) (2024-07-09)
15
+
16
+
17
+ ### Features
18
+
19
+ * **BTC:** add changeAddressType to createAsset config ([#2772](https://github.com/ExodusMovement/assets/issues/2772)) ([d8e38dd](https://github.com/ExodusMovement/assets/commit/d8e38dddfbe59b3e8ff9fc2432049bb512a91c22))
20
+
21
+
22
+
6
23
  ## [2.19.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.18.4...@exodus/bitcoin-api@2.19.0) (2024-07-05)
7
24
 
8
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.19.0",
3
+ "version": "2.20.1",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "9c3024f69fbda1069a437fb19c43e6f341185c6f"
69
+ "gitHead": "e1354557f99e81651fad1b25a4f72183587324b6"
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'
@@ -19,6 +19,7 @@ const _canBumpTx = ({
19
19
  allowUnconfirmedRbfEnabledUtxos,
20
20
  utxosDescendingOrder,
21
21
  taprootInputWitnessSize,
22
+ changeAddressType,
22
23
  }) => {
23
24
  assert(asset, 'asset must be provided')
24
25
  assert(tx, 'tx must be provided')
@@ -95,6 +96,7 @@ const _canBumpTx = ({
95
96
  unconfirmedTxAncestor,
96
97
  utxosDescendingOrder,
97
98
  taprootInputWitnessSize,
99
+ changeAddressType,
98
100
  })
99
101
  if (replaceTx) return { bumpType: BumpType.RBF, bumpFee: fee.sub(replaceTx.feeAmount) }
100
102
  }
@@ -103,13 +105,14 @@ const _canBumpTx = ({
103
105
  asset,
104
106
  usableUtxos,
105
107
  feeRate,
106
- receiveAddress: 'P2WPKH',
108
+ receiveAddress: changeAddressType,
107
109
  getFeeEstimator,
108
110
  mustSpendUtxos: changeUtxos,
109
111
  allowUnconfirmedRbfEnabledUtxos,
110
112
  unconfirmedTxAncestor,
111
113
  utxosDescendingOrder,
112
114
  taprootInputWitnessSize,
115
+ changeAddressType,
113
116
  })
114
117
 
115
118
  return fee ? { bumpType: BumpType.CPFP, bumpFee: fee } : { errorMessage: 'insufficient funds' }
@@ -15,13 +15,20 @@ export class GetFeeResolver {
15
15
  #getFeeEstimator
16
16
  #allowUnconfirmedRbfEnabledUtxos
17
17
  #utxosDescendingOrder
18
+ #changeAddressType
18
19
 
19
- constructor({ getFeeEstimator, allowUnconfirmedRbfEnabledUtxos, utxosDescendingOrder }) {
20
+ constructor({
21
+ getFeeEstimator,
22
+ allowUnconfirmedRbfEnabledUtxos,
23
+ utxosDescendingOrder,
24
+ changeAddressType,
25
+ }) {
20
26
  assert(getFeeEstimator, 'getFeeEstimator must be provided')
21
27
  this.#getFeeEstimator = (asset, { feePerKB, ...options }) =>
22
28
  getFeeEstimator(asset, feePerKB, options)
23
29
  this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
24
30
  this.#utxosDescendingOrder = utxosDescendingOrder
31
+ this.#changeAddressType = changeAddressType
25
32
  }
26
33
 
27
34
  getFee = ({
@@ -151,10 +158,11 @@ export class GetFeeResolver {
151
158
  unconfirmedTxAncestor,
152
159
  utxosDescendingOrder: this.#utxosDescendingOrder,
153
160
  taprootInputWitnessSize,
161
+ changeAddressType: this.#changeAddressType,
154
162
  })
155
163
  }
156
164
 
157
- canBumpTx = ({ asset, tx, txSet, accountState, feeData }) => {
165
+ canBumpTx = ({ asset, tx, txSet, accountState, feeData, taprootInputWitnessSize }) => {
158
166
  return canBumpTx({
159
167
  asset,
160
168
  tx,
@@ -164,6 +172,8 @@ export class GetFeeResolver {
164
172
  getFeeEstimator: this.#getFeeEstimator,
165
173
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
166
174
  utxosDescendingOrder: this.#utxosDescendingOrder,
175
+ taprootInputWitnessSize,
176
+ changeAddressType: this.#changeAddressType,
167
177
  })
168
178
  }
169
179
  }
@@ -36,6 +36,7 @@ export const selectUtxos = ({
36
36
  inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
37
37
  transferOrdinalsUtxos, // to calculate the size of the input
38
38
  taprootInputWitnessSize,
39
+ changeAddressType = 'P2PKH',
39
40
  }) => {
40
41
  const resolvedReceiveAddresses = getBestReceiveAddresses({
41
42
  asset,
@@ -54,10 +55,6 @@ export const selectUtxos = ({
54
55
  assert(usableUtxos, 'usableUtxos is required')
55
56
  assert(getFeeEstimator, 'getFeeEstimator is required')
56
57
 
57
- const changeAddressType = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
58
- ? 'P2WPKH'
59
- : 'P2PKH'
60
-
61
58
  const feeEstimator = getFeeEstimator(asset, { feePerKB: feeRate, unconfirmedTxAncestor })
62
59
  const { currency } = asset
63
60
  if (!amount) amount = currency.ZERO
@@ -263,6 +260,7 @@ export const getUtxosData = ({
263
260
  unconfirmedTxAncestor,
264
261
  utxosDescendingOrder,
265
262
  taprootInputWitnessSize,
263
+ changeAddressType,
266
264
  }) => {
267
265
  const { selectedUtxos, replaceTx, fee } = selectUtxos({
268
266
  asset,
@@ -281,6 +279,7 @@ export const getUtxosData = ({
281
279
  transferOrdinalsUtxos,
282
280
  utxosDescendingOrder,
283
281
  taprootInputWitnessSize,
282
+ changeAddressType,
284
283
  })
285
284
 
286
285
  const resolvedFee = replaceTx ? fee.sub(replaceTx.feeAmount) : fee
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,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)
@@ -281,6 +281,12 @@ export class Monitor extends BaseMonitor {
281
281
  newData.mem = { unconfirmedTxAncestor }
282
282
  }
283
283
 
284
+ if (this.fetchFungibleBalances) {
285
+ try {
286
+ newData.magicEdenApiFungibleBalances = await this.fetchFungibleBalances(walletAccount)
287
+ } catch {}
288
+ }
289
+
284
290
  await aci.updateAccountState({
285
291
  assetName,
286
292
  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
@@ -223,6 +223,7 @@ export const getPrepareSendTransaction =
223
223
  utxosDescendingOrder,
224
224
  rbfEnabled: providedRbfEnabled,
225
225
  assetClientInterface,
226
+ changeAddressType,
226
227
  }) =>
227
228
  async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
228
229
  const {
@@ -243,6 +244,7 @@ export const getPrepareSendTransaction =
243
244
  const assetName = asset.name
244
245
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
245
246
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
247
+ feeData.feePerKB = feePerKB ?? feeData.feePerKB
246
248
  const insightClient = asset.baseAsset.insightClient
247
249
 
248
250
  const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
@@ -354,6 +356,7 @@ export const getPrepareSendTransaction =
354
356
  transferOrdinalsUtxos,
355
357
  utxosDescendingOrder,
356
358
  taprootInputWitnessSize,
359
+ changeAddressType,
357
360
  })
358
361
 
359
362
  if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
@@ -473,18 +476,11 @@ export const createAndBroadcastTXFactory =
473
476
  ordinalsEnabled = false,
474
477
  utxosDescendingOrder,
475
478
  assetClientInterface,
479
+ changeAddressType,
476
480
  }) =>
477
481
  async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
478
482
  // Prepare transaction
479
- const {
480
- bumpTxId,
481
- nft,
482
- isExchange,
483
- isBip70,
484
- isRbfAllowed = true,
485
- feeOpts,
486
- taprootInputWitnessSize,
487
- } = options
483
+ const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true, feeOpts } = options
488
484
 
489
485
  const asset = maybeToken.baseAsset
490
486
  const assetName = asset.name
@@ -508,7 +504,7 @@ export const createAndBroadcastTXFactory =
508
504
  utxosDescendingOrder,
509
505
  rbfEnabled,
510
506
  assetClientInterface,
511
- taprootInputWitnessSize,
507
+ changeAddressType,
512
508
  })({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options })
513
509
  const {
514
510
  amount,
@@ -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