@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 +22 -0
- package/package.json +3 -3
- package/src/account-state.js +10 -1
- package/src/balances.js +14 -0
- package/src/btc-like-keys.js +1 -1
- package/src/fee/utxo-selector.js +2 -2
- package/src/index.js +2 -0
- package/src/move-funds.js +1 -1
- package/src/psbt-utils.js +61 -0
- package/src/tx-log/bitcoin-me-monitor.js +78 -0
- package/src/tx-log/bitcoin-monitor.js +12 -0
- package/src/tx-log/index.js +1 -0
- package/src/tx-log/magic-eden-bitcoin-monitor.js +86 -0
- package/src/tx-log/me-flagr.js +22 -0
- package/src/tx-send/batch-tx.js +206 -0
- package/src/tx-send/index.js +8 -4
- package/src/tx-sign/create-get-key-and-purpose.js +1 -1
- package/src/tx-sign/default-entropy.cjs +6 -0
- package/src/tx-sign/taproot.js +5 -3
- package/src/tx-sign/default-entropy.js +0 -2
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.
|
|
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": "
|
|
69
|
+
"gitHead": "ba7439e1b8ae182a2a95ffd0ea09b8bc3330f2f6"
|
|
70
70
|
}
|
package/src/account-state.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AccountState, UtxoCollection } from '@exodus/models'
|
|
2
2
|
|
|
3
|
-
export function createAccountState({
|
|
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({
|
package/src/btc-like-keys.js
CHANGED
|
@@ -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
|
|
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'
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -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
|
|
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,
|
package/src/tx-log/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { crypto } from '@exodus/bitcoinjs-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
// defaultEntropy.getSchnorrEntropy() is mockable with jest.spyOn
|
|
99
|
+
extraEntropy: defaultEntropy.getSchnorrEntropy(),
|
|
98
100
|
})
|
|
99
101
|
},
|
|
100
102
|
publicKey,
|